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:
2026-10-23 16:41:45 +07:00
parent 65a001dad4
commit c8bc4a95a9
54 changed files with 2521 additions and 112 deletions
+33 -4
View File
@@ -17,6 +17,7 @@ const { db } = require("../db");
const lanesLib = require("../lib/lanes");
const { sessionWorktree, removeSessionWorktree } = require("../lib/session-worktree");
const { sourceFolder } = require("../lib/lane-sources");
const { nestedRepos, repoFolder } = require("../lib/nested-repos");
const { suggestCommitMessage } = require("../lib/commit-message");
const aiTools = require("../lib/ai-tools");
const {
@@ -1315,6 +1316,23 @@ router.get("/:id/integrations/:name", (req, res) => {
* POST and Express matches on method as well as path. Verified by moving this
* registration after it: the suite stayed green.
*/
/**
* The git repositories inside the workspace folder (`server/lib/nested-repos.js`):
* a parent folder of several repos gets one Changes-panel section per repo,
* each read and written through `?repo=<path>`. `root` says whether the
* workspace folder is itself in a repo. Disk reads only, cached 30 s;
* `?refresh=1` reads again. A missing folder is an empty list, not a fault.
*/
router.get("/:id/repos", async (req, res) => {
const lane = laneOr404(req, res);
if (!lane) return;
try {
res.json(await nestedRepos(lane.cwd, { refresh: req.query.refresh === "1" }));
} catch {
res.json({ root: false, items: [], truncated: false });
}
});
router.get("/:id/git", async (req, res) => {
const t = await gitTargetOr404(req, res);
if (!t) return;
@@ -2426,14 +2444,25 @@ function gitErrorCode(err) {
* `?session=<id>` — that session's worktree (`claude --worktree`), resolved by
* `sessionWorktree` from the session ROW (owned by this workspace, registered
* by `git worktree list`), or — with `?source=<id>` — one of the workspace's
* extra folders, resolved by `sourceFolder` from its `lane_sources` ROW; never
* from a path in the request. An unresolvable session (400) or source (404,
* like another workspace's row) is never a silent fall back to the main
* folder: a commit or a discard must never land in the wrong tree.
* extra folders, resolved by `sourceFolder` from its `lane_sources` ROW, or —
* with `?repo=<path>` — a repo inside the workspace folder, accepted only when
* `repoFolder`'s own walk of that folder found it; never from a path in the
* request. An unresolvable session (400), source or repo (404, like another
* workspace's row) is never a silent fall back to the main folder: a commit
* or a discard must never land in the wrong tree.
*/
async function gitTargetOr404(req, res) {
const lane = laneOr404(req, res);
if (!lane) return null;
if (req.query.repo !== undefined) {
try {
const cwd = await repoFolder(lane.cwd, req.query.repo);
return { lane, cwd, worktree: null, source: true };
} catch (err) {
res.status(404).json({ error: { code: err.code, message: err.message } });
return null;
}
}
if (req.query.source !== undefined) {
try {
const cwd = sourceFolder(lane.id, req.query.source);