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:
@@ -85,6 +85,7 @@ import {
|
||||
type FsTrashItem,
|
||||
type FsHome,
|
||||
type FsPlace,
|
||||
type GitSource,
|
||||
isHomePlace,
|
||||
} from "../../lib/api";
|
||||
import type { Lane } from "../../lib/types";
|
||||
@@ -283,7 +284,7 @@ export function FileBrowser({
|
||||
/** A home to open on (`me`, or another user's for an admin). */
|
||||
initialHome?: string | null;
|
||||
session?: string | null;
|
||||
source?: number | null;
|
||||
source?: GitSource | null;
|
||||
initialPath?: string;
|
||||
/** Called after anything changed files, so a tree elsewhere can re-read. */
|
||||
onChanged?: () => void;
|
||||
@@ -1882,7 +1883,7 @@ export function FileExplorer({
|
||||
onClose: () => void;
|
||||
laneId: number;
|
||||
session?: string | null;
|
||||
source?: number | null;
|
||||
source?: GitSource | null;
|
||||
initialPath?: string;
|
||||
onChanged?: () => void;
|
||||
}) {
|
||||
|
||||
@@ -103,7 +103,7 @@ import {
|
||||
Trash2,
|
||||
UnfoldVertical,
|
||||
} from "lucide-react";
|
||||
import { api, gitTarget } from "../../lib/api";
|
||||
import { api, gitTarget, type GitSource } from "../../lib/api";
|
||||
import { usePermissions } from "../AuthGate";
|
||||
import { pasteToLane } from "../../lib/panePaste";
|
||||
import { readMarkdownPreview } from "../../lib/markdownView";
|
||||
@@ -827,7 +827,7 @@ export function LaneChangesPanel({
|
||||
session?: string;
|
||||
/** Show and act on that extra folder's repo instead (`lane_sources` id);
|
||||
* wins over `session`, and is keyed on like it. */
|
||||
source?: number;
|
||||
source?: GitSource;
|
||||
/** Open the Add-lane flow for a sibling worktree; see `LaneGitBar`. */
|
||||
onNewLane?: (branch?: string) => void;
|
||||
/** The panel has the whole width: the file tree becomes a sidebar and a
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { api, gitTarget } from "../../lib/api";
|
||||
import { api, gitTarget, type GitSource } from "../../lib/api";
|
||||
import {
|
||||
applyChoices,
|
||||
parseConflicts,
|
||||
@@ -47,7 +47,7 @@ export function LaneConflictView({
|
||||
/** Work in that session's own worktree (see `gitTarget`). */
|
||||
session?: string;
|
||||
/** Work in that extra folder's repo instead (`lane_sources` id, see `gitTarget`). */
|
||||
source?: number;
|
||||
source?: GitSource;
|
||||
path: string;
|
||||
/** The file left the conflict (resolved, or a whole side taken). */
|
||||
onResolved: () => void;
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
Trash2,
|
||||
Upload,
|
||||
} from "lucide-react";
|
||||
import { api, gitTarget, type GitAuthNeeded } from "../../lib/api";
|
||||
import { api, gitTarget, type GitSource, type GitAuthNeeded } from "../../lib/api";
|
||||
import { toast } from "../../lib/toast";
|
||||
import { confirmDialog } from "../../lib/confirm";
|
||||
import { isSubmitEnter } from "../../lib/keys";
|
||||
@@ -98,7 +98,7 @@ export function LaneGitBar({
|
||||
/** Work in that session's own worktree (see `gitTarget`). */
|
||||
session?: string;
|
||||
/** Work in that extra folder's repo instead (`lane_sources` id, see `gitTarget`). */
|
||||
source?: number;
|
||||
source?: GitSource;
|
||||
git: LaneGitFacts;
|
||||
/** Rendered at the start of the action row: the header's branch line, so
|
||||
* the facts and the buttons that act on them read as one row. */
|
||||
|
||||
@@ -19,7 +19,7 @@ import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { GitBranch } from "lucide-react";
|
||||
import { LaneGitBar } from "./LaneGitBar";
|
||||
import { api, gitTarget } from "../../lib/api";
|
||||
import { api, gitTarget, type GitSource } from "../../lib/api";
|
||||
import { keepIfSame } from "../../lib/keepIfSame";
|
||||
import type { LaneGitFacts, LaneGitFile } from "../../lib/types";
|
||||
|
||||
@@ -37,8 +37,8 @@ export function useLaneGitFacts(
|
||||
laneId: number,
|
||||
/** Read that session's own worktree instead (`?session=`). */
|
||||
session?: string,
|
||||
/** Read that extra folder's repo instead (`?source=`). */
|
||||
source?: number
|
||||
/** Read that extra folder's repo (`?source=`, a number) or that inner repo (`?repo=`, a string) instead. */
|
||||
source?: GitSource
|
||||
): [LaneGitFacts | null, () => void] {
|
||||
const [facts, setFacts] = useState<LaneGitFacts | null>(null);
|
||||
// Bumped by the git action bar: a commit or a pull changes every fact on
|
||||
@@ -329,7 +329,7 @@ export function LaneGitRailCounts({
|
||||
}: {
|
||||
laneId: number;
|
||||
session?: string;
|
||||
source?: number;
|
||||
source?: GitSource;
|
||||
/** Opens the Changes panel from the file list's "… more files". */
|
||||
onShowAll?: () => void;
|
||||
}) {
|
||||
@@ -349,7 +349,7 @@ export function LaneGitHeader({
|
||||
/** Work in that session's own worktree (see `gitTarget`). */
|
||||
session?: string;
|
||||
/** Work in that extra folder's repo instead (`lane_sources` id, see `gitTarget`). */
|
||||
source?: number;
|
||||
source?: GitSource;
|
||||
git: LaneGitFacts | null;
|
||||
/** Called after a git bar action: the facts AND the panel's diff moved. */
|
||||
onChanged: () => void;
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
* the pointer (right-click) or under the row's `⋯` button. Every item is an
|
||||
* existing call: Rename (the row's inline field), New workspace here (the
|
||||
* Add modal prefilled), Copy path, and Reset/Remove through `DestructiveLaneModal` — Reset only on a
|
||||
* managed lane. The server's guards are the real ones; the menu only hides
|
||||
* managed lane — plus Move to group (the sidebar's own groups,
|
||||
* `lib/workspaceGroups.ts`: one of them, a new one, or out of its group). The
|
||||
* server's guards are the real ones; the menu only hides
|
||||
* what the lane's kind or tool makes meaningless. The shell (position,
|
||||
* Escape / outside-click close) is `ContextMenu`.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
@@ -11,7 +13,9 @@
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Lane } from "../../lib/types";
|
||||
import { Check } from "lucide-react";
|
||||
import { ContextMenu, MenuItem, MenuSeparator } from "../ContextMenu";
|
||||
import type { WorkspaceGroup } from "../../lib/workspaceGroups";
|
||||
import { copyText } from "../../lib/clipboard";
|
||||
|
||||
export type WorkspaceMenuAction = "rename" | "new-here" | "reset" | "remove";
|
||||
@@ -22,6 +26,9 @@ export function WorkspaceRowMenu({
|
||||
y,
|
||||
onClose,
|
||||
onAction,
|
||||
groups = [],
|
||||
groupId = null,
|
||||
onGroup,
|
||||
}: {
|
||||
lane: Lane;
|
||||
x: number;
|
||||
@@ -29,6 +36,11 @@ export function WorkspaceRowMenu({
|
||||
onClose: () => void;
|
||||
/** Items the tree itself carries out (rename field, modals). */
|
||||
onAction: (action: WorkspaceMenuAction) => void;
|
||||
/** The sidebar's groups and the one this workspace is in. */
|
||||
groups?: WorkspaceGroup[];
|
||||
groupId?: string | null;
|
||||
/** Move it: to a group's id, out of its group (null), or into a new group ("new"). */
|
||||
onGroup?: (target: string | null | "new") => void;
|
||||
}) {
|
||||
const { t } = useTranslation("lanes");
|
||||
return (
|
||||
@@ -43,6 +55,35 @@ export function WorkspaceRowMenu({
|
||||
>
|
||||
{t("menu.copyPath")}
|
||||
</MenuItem>
|
||||
{onGroup && (
|
||||
<>
|
||||
<MenuSeparator />
|
||||
<div className="px-2 pb-0.5 pt-1 text-[10px] font-semibold uppercase tracking-wide text-fg-muted">
|
||||
{t("menu.moveToGroup")}
|
||||
</div>
|
||||
{groups.map((g) => (
|
||||
<MenuItem
|
||||
key={g.id}
|
||||
testId={`workspace-move-group-${g.id}`}
|
||||
disabled={g.id === groupId}
|
||||
onSelect={() => onGroup(g.id)}
|
||||
>
|
||||
<Check className={`h-3 w-3 shrink-0 ${g.id === groupId ? "" : "invisible"}`} />
|
||||
<span className="truncate">{g.name}</span>
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem testId="workspace-move-group-new" onSelect={() => onGroup("new")}>
|
||||
<span className="w-3 shrink-0" />
|
||||
{t("menu.newGroupWith")}
|
||||
</MenuItem>
|
||||
{groupId && (
|
||||
<MenuItem testId="workspace-move-group-none" onSelect={() => onGroup(null)}>
|
||||
<span className="w-3 shrink-0" />
|
||||
{t("menu.removeFromGroup")}
|
||||
</MenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<MenuSeparator />
|
||||
{lane.kind === "managed" && (
|
||||
<MenuItem danger onSelect={() => onAction("reset")}>
|
||||
|
||||
Reference in New Issue
Block a user