feat: workspace aliases, tab groups of split panes, native path picker (v2.1.0)
- /api/workspaces and `agenthub workspace(s)` / `--workspace` as aliases of /api/lanes and `agenthub lanes` / `--lane`; old names print one stderr note. REPL `help lanes|workspace|workspaces` resolves through the aliases. - A workspace holds up to ten sessions (slots 0-9); every console tab is a group of 1-4 split panes (PaneGroups, agenthub.workspace.paneLayout); parseGroups keeps the stored active tab when an earlier one is dropped. - Browse button beside path fields: POST /api/lanes/pick-path opens the OS picker on the server's desktop — loopback only and no forwarding header (EREMOTE), a display failure is ENODIALOG not a Cancel, the dialog is killed when the request closes, a folder field falls back to the in-app browser on any failure. - Changes rail counts chip, image viewer (/git/raw), status bar session counts and per-day cost (`date=` on /api/pricing/cost). - /agenthub-open and /agenthub-doctor run from the INSTALLED build (supersededBy), doctor offers fixes behind two confirmations; plugin-open treats a locked bootstrap as non-fatal. - New-terminal shell waits for the first attach (issue #55). - repo-onboarding / debug-live-issue skills removed. - OpenAPI: pick-path, lane/date on /pricing/cost, tool on /run/history, /api/workspaces mount.
This commit is contained in:
+84
-43
@@ -1291,7 +1291,7 @@ async function cmdDoctor() {
|
||||
console.log(
|
||||
available
|
||||
? `${c.green("✔")} tmux available to the server`
|
||||
: `${c.yellow("!")} tmux not on the server's PATH — Start Run and \`agenthub lanes shell\` will fail. ` +
|
||||
: `${c.yellow("!")} tmux not on the server's PATH — Start Run and \`agenthub workspace shell\` will fail. ` +
|
||||
`Install it (sudo apt install tmux / brew install tmux), then restart the server`
|
||||
);
|
||||
} catch {
|
||||
@@ -1616,7 +1616,7 @@ async function cmdLanesAdd(args) {
|
||||
if (repo) {
|
||||
if (cwd || pipeline) {
|
||||
console.error(
|
||||
"usage: agenthub lanes add --repo <path> [--title <text>] [--base <branch>] [--slug <slug>] [--tool <id>]"
|
||||
"usage: agenthub workspace add --repo <path> [--title <text>] [--base <branch>] [--slug <slug>] [--tool <id>]"
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
@@ -1653,7 +1653,7 @@ async function cmdLanesAdd(args) {
|
||||
}
|
||||
if (!cwd || !title) {
|
||||
console.error(
|
||||
"usage: agenthub lanes add --cwd <path> --title <text> [--pipeline <id>] [--tool <id>]"
|
||||
"usage: agenthub workspace add --cwd <path> --title <text> [--pipeline <id>] [--tool <id>]"
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
@@ -1704,7 +1704,7 @@ function cmdLanesProfileInit(args) {
|
||||
const repo = args.find((arg) => !arg.startsWith("--"));
|
||||
const force = args.includes("--force");
|
||||
if (!repo) {
|
||||
console.error("usage: agenthub lanes profile init <repo> [--force]");
|
||||
console.error("usage: agenthub workspace profile init <repo> [--force]");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -1746,7 +1746,7 @@ function cmdLanesProfileInit(args) {
|
||||
console.log(`\n${c.yellow(`⚠ ${result.todos.length} item(s) need manual attention:`)}`);
|
||||
for (const todo of result.todos) console.log(` ${todo}`);
|
||||
}
|
||||
console.log(`\nNext: agenthub lanes profile check ${repo}`);
|
||||
console.log(`\nNext: agenthub workspace profile check ${repo}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1795,7 +1795,7 @@ async function cmdLanesLifecycle(action, args) {
|
||||
const fields = LANE_PREFLIGHT_FIELDS[action];
|
||||
if (!id) {
|
||||
const extra = action === "reset" ? " [--keep-db]" : "";
|
||||
console.error(`usage: agenthub lanes ${action} <id> [--force]${extra} --yes`);
|
||||
console.error(`usage: agenthub workspace ${action} <id> [--force]${extra} --yes`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -1834,7 +1834,7 @@ async function cmdLanesLifecycle(action, args) {
|
||||
preflight.blocked.includes("adopted")
|
||||
) {
|
||||
console.error(
|
||||
`Workspace #${id} points at a directory you own. It cannot be reset; use \`agenthub lanes remove ${id} --yes\` to drop only the dashboard's record of it.`
|
||||
`Workspace #${id} points at a directory you own. It cannot be reset; use \`agenthub workspace remove ${id} --yes\` to drop only the dashboard's record of it.`
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
@@ -1902,7 +1902,7 @@ async function cmdLanes() {
|
||||
const { lanes, counts } = await get("/api/lanes");
|
||||
if (!lanes.length) {
|
||||
console.log(
|
||||
"no workspaces yet — create one with: agenthub lanes add --cwd <path> --title <text>"
|
||||
"no workspaces yet — create one with: agenthub workspace add --cwd <path> --title <text>"
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -1957,7 +1957,7 @@ async function resolveLaneArg(args) {
|
||||
return i > -1 ? args[i + 1] : undefined;
|
||||
};
|
||||
|
||||
const explicit = flagValue("lane");
|
||||
const explicit = flagValue("lane") || flagValue("workspace");
|
||||
if (explicit) return { laneId: explicit, rest: args };
|
||||
if (args.length && /^\d+$/.test(args[0])) return { laneId: args[0], rest: args.slice(1) };
|
||||
|
||||
@@ -1981,7 +1981,9 @@ async function resolveLaneArg(args) {
|
||||
.filter((l) => cwd === l.cwd || cwd.startsWith(`${l.cwd}/`))
|
||||
.sort((a, b) => b.cwd.length - a.cwd.length);
|
||||
if (!owning.length) {
|
||||
console.error(`no workspace owns ${cwd} — create one with: agenthub lanes add --cwd ${cwd}`);
|
||||
console.error(
|
||||
`no workspace owns ${cwd} — create one with: agenthub workspace add --cwd ${cwd}`
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return null;
|
||||
}
|
||||
@@ -2002,7 +2004,7 @@ async function resolveLaneArg(args) {
|
||||
console.error(
|
||||
`${candidates.length} workspaces own ${owning[0].cwd} — pass --tool <${candidates
|
||||
.map((l) => l.tool || "claude")
|
||||
.join("|")}> or --lane <id>: ${candidates
|
||||
.join("|")}> or --workspace <id>: ${candidates
|
||||
.map((l) => `#${l.id} (${l.tool || "claude"})`)
|
||||
.join(", ")}`
|
||||
);
|
||||
@@ -2020,7 +2022,7 @@ function printRuntime(runtime) {
|
||||
return;
|
||||
}
|
||||
if (!runtime.provisioned) {
|
||||
console.log("profile found, runtime not provisioned yet — run: agenthub lanes up");
|
||||
console.log("profile found, runtime not provisioned yet — run: agenthub workspace up");
|
||||
return;
|
||||
}
|
||||
console.log(`slot ${runtime.slot} · profile ${runtime.profileDir}`);
|
||||
@@ -2121,7 +2123,7 @@ async function cmdLanesRuntime(sub, args) {
|
||||
if (sub === "logs") {
|
||||
const svc = laneArgs.find((arg) => !arg.startsWith("--"));
|
||||
if (!svc) {
|
||||
console.error("usage: agenthub lanes logs [<id>] <service> [--tail bytes]");
|
||||
console.error("usage: agenthub workspace logs [<id>] <service> [--tail bytes]");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -2140,7 +2142,7 @@ async function cmdLanesRuntime(sub, args) {
|
||||
if (sub === "hook") {
|
||||
const name = laneArgs[0];
|
||||
if (!name || name.startsWith("--")) {
|
||||
console.error("usage: agenthub lanes hook [<id>] <name> [args…]");
|
||||
console.error("usage: agenthub workspace hook [<id>] <name> [args…]");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -2155,7 +2157,7 @@ async function cmdLanesRuntime(sub, args) {
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
`workspace #${laneId} running hook ${name} — follow it with: agenthub lanes logs ${laneId} ${name}`
|
||||
`workspace #${laneId} running hook ${name} — follow it with: agenthub workspace logs ${laneId} ${name}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2191,7 +2193,7 @@ async function cmdLanesRuntime(sub, args) {
|
||||
console.error(` conflicted: ${result.conflictedFiles.join(", ")}`);
|
||||
console.error(" resolve, then: git add <resolved files> && git commit --no-edit");
|
||||
console.error(
|
||||
` then: agenthub lanes sync-base --continue ${branch ? branch + " " : ""}${laneId}`
|
||||
` then: agenthub workspace sync-base --continue ${branch ? branch + " " : ""}${laneId}`
|
||||
);
|
||||
process.exitCode = 4;
|
||||
return;
|
||||
@@ -2437,7 +2439,7 @@ async function cmdLanesMcpSync(args) {
|
||||
async function cmdLanesIntegration(args) {
|
||||
const name = args.find((arg) => !arg.startsWith("--"));
|
||||
if (!name) {
|
||||
console.error("usage: agenthub lanes integration <name> [<id>]");
|
||||
console.error("usage: agenthub workspace integration <name> [<id>]");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -2456,13 +2458,13 @@ async function cmdLanesIntegration(args) {
|
||||
* mirrors the derived state: 0 success, 1 failure or any error, 3 pending. */
|
||||
async function cmdLanesCi(args) {
|
||||
if (args[0] !== "status") {
|
||||
console.error("usage: agenthub lanes ci status <branch-or-sha> [<id>]");
|
||||
console.error("usage: agenthub workspace ci status <branch-or-sha> [<id>]");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const ref = args[1];
|
||||
if (!ref) {
|
||||
console.error("usage: agenthub lanes ci status <branch-or-sha> [<id>]");
|
||||
console.error("usage: agenthub workspace ci status <branch-or-sha> [<id>]");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -2538,7 +2540,7 @@ async function cmdLanesNode(args) {
|
||||
|
||||
if (!key) {
|
||||
listNodes();
|
||||
console.error(`\nusage: agenthub lanes node <node-id> [extra args…] [<id>] [--print]`);
|
||||
console.error(`\nusage: agenthub workspace node <node-id> [extra args…] [<id>] [--print]`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -2683,7 +2685,7 @@ async function cmdStage(args) {
|
||||
const stage = args[0];
|
||||
if (!stage || stage.startsWith("--")) {
|
||||
console.error(
|
||||
"usage: agenthub stage <stage> [--lane <id>] [--cwd <path>] [--tool <id>] [--status <s>] [--evidence <text>] [--note <text>] [--result pass|fail]"
|
||||
"usage: agenthub stage <stage> [--workspace <id>] [--cwd <path>] [--tool <id>] [--status <s>] [--evidence <text>] [--note <text>] [--result pass|fail]"
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
@@ -2805,82 +2807,86 @@ const COMMAND_GROUPS = [
|
||||
[
|
||||
"Workspaces",
|
||||
[
|
||||
["lanes", "", "List workspaces with stage, liveness and progress"],
|
||||
["workspaces", "", "List workspaces with stage, liveness and progress"],
|
||||
[
|
||||
"lanes add",
|
||||
"workspace add",
|
||||
"--cwd <path> --title <text> [--tool <id>]",
|
||||
"Create a new workspace bound to a directory (--tool: an AI Tools id, default claude)",
|
||||
],
|
||||
[
|
||||
"lanes add",
|
||||
"workspace add",
|
||||
"--repo <path> [--title <text>] [--base <branch>] [--slug <slug>] [--tool <id>]",
|
||||
"Provision a managed worktree workspace",
|
||||
],
|
||||
[
|
||||
"lanes profile init",
|
||||
"workspace profile init",
|
||||
"<repo> [--force]",
|
||||
"Detect a Node.js project and scaffold .agenthub/profile/",
|
||||
],
|
||||
[
|
||||
"lanes profile check",
|
||||
"workspace profile check",
|
||||
"[<path>]",
|
||||
"Validate a profile (path defaults to cwd, not a workspace id)",
|
||||
],
|
||||
[
|
||||
"lanes pipeline",
|
||||
"workspace pipeline",
|
||||
"[<template-id>] [<id>]",
|
||||
"Show, or switch, which pipeline template a workspace renders against",
|
||||
],
|
||||
[
|
||||
"lanes node",
|
||||
"workspace node",
|
||||
"[<node-id>] [extra…] [<id>] [--print]",
|
||||
"Run ONE pipeline node's own command in the workspace's cwd (no id: list every node and its command)",
|
||||
],
|
||||
[
|
||||
"lanes shell",
|
||||
"workspace shell",
|
||||
"[<id>]",
|
||||
"Attach a real terminal to the exact tmux session the dashboard uses for a workspace's run",
|
||||
],
|
||||
[
|
||||
"lanes reset|remove|purge",
|
||||
"workspace reset|remove|purge",
|
||||
"<id> [--force] [--keep-db] --yes",
|
||||
"Show preflight facts, then perform a destructive workspace action (--keep-db: reset only; remove drops the dashboard's record only — worktree, branch and database are left alone)",
|
||||
],
|
||||
[
|
||||
"lanes up|down",
|
||||
"workspace up|down",
|
||||
"[<id>] [--no-build] [--qc]",
|
||||
"Boot or stop the workspace's own app stack (--no-build: up only, skip the build step; --qc: up only, inject QC_BOOT_ENV for a deterministic stack; id defaults to the workspace owning this directory)",
|
||||
],
|
||||
["lanes runtime", "[<id>]", "Slot, ports, service health and the last boot error"],
|
||||
["lanes logs", "[<id>] <svc> [--tail N]", "Tail one of the workspace's hook or service logs"],
|
||||
["lanes hook", "[<id>] <name> [args…]", "Run a profile hook (ci-gate, e2e, migrate, …)"],
|
||||
["workspace runtime", "[<id>]", "Slot, ports, service health and the last boot error"],
|
||||
[
|
||||
"lanes sync-base",
|
||||
"workspace logs",
|
||||
"[<id>] <svc> [--tail N]",
|
||||
"Tail one of the workspace's hook or service logs",
|
||||
],
|
||||
["workspace hook", "[<id>] <name> [args…]", "Run a profile hook (ci-gate, e2e, migrate, …)"],
|
||||
[
|
||||
"workspace sync-base",
|
||||
"[<id>] [--check|--continue] [branch]",
|
||||
"Fetch + migration-collision preflight, or merge origin/development into the feature branch (--check: read-only; --continue: finish after a resolved conflict; bare: merge, branch defaults to the workspace's current branch)",
|
||||
],
|
||||
[
|
||||
"lanes proof-link",
|
||||
"workspace proof-link",
|
||||
"[<id>]",
|
||||
"Converge clone-root proof/ onto .playwright-mcp/proof (idempotent, never automatic)",
|
||||
],
|
||||
[
|
||||
"lanes mcp sync",
|
||||
"workspace mcp sync",
|
||||
"[<id>]",
|
||||
"Relocate the source repo's MCP servers into <workspace>/.mcp.json and seed Chromium profiles",
|
||||
],
|
||||
[
|
||||
"lanes integration",
|
||||
"workspace integration",
|
||||
"<name> [<id>]",
|
||||
"Check whether a named integration (tracker, dev_qc, ci_wait) is on for this workspace — exit 0/1",
|
||||
],
|
||||
[
|
||||
"lanes ci status",
|
||||
"workspace ci status",
|
||||
"<branch-or-sha> [<id>]",
|
||||
"One-shot GitHub Actions status for a ref — exit 0 success / 1 failure / 3 pending",
|
||||
],
|
||||
[
|
||||
"lanes gc",
|
||||
"workspace gc",
|
||||
"[--dry-run]",
|
||||
"Reap orphaned Playwright MCP processes + cap oversized hook logs, across every workspace",
|
||||
],
|
||||
@@ -2944,6 +2950,9 @@ falling back to http://127.0.0.1:4820.
|
||||
${c.bold("Output:")} colors auto-enable on a TTY and turn off when piped.
|
||||
Disable with --no-color or NO_COLOR=1; force with FORCE_COLOR=1 / AGENTHUB_COLOR=1.
|
||||
|
||||
${c.bold("Old name:")} ${c.bold("agenthub lanes …")} and ${c.bold("--lane")} still work — the same
|
||||
commands as ${c.bold("agenthub workspace …")} and ${c.bold("--workspace")}.
|
||||
|
||||
${c.bold("Note:")} agenthub talks to the local dashboard server — API-backed commands
|
||||
require it to be running (${c.bold("agenthub start")} brings one up in the background).
|
||||
Prefer an interactive session? Run ${c.bold("agenthub repl")} for a shell with completion,
|
||||
@@ -3352,10 +3361,18 @@ function replHelp() {
|
||||
/** Detailed help for one command (or command prefix): every catalog entry
|
||||
* whose invocation starts with the token. Returns false if none matched. */
|
||||
function commandHelp(token) {
|
||||
// `lanes`, `workspace` and `workspaces` are one command (COMMAND_ALIASES),
|
||||
// so `help lanes` finds the `workspace …` rows and vice versa.
|
||||
const canon = (words) => {
|
||||
const [head, ...tail] = words.split(" ");
|
||||
return [COMMAND_ALIASES[head] || head, ...tail].join(" ");
|
||||
};
|
||||
const want = canon(token);
|
||||
const matches = [];
|
||||
for (const [group, items] of COMMAND_GROUPS) {
|
||||
for (const [name, args, desc] of items) {
|
||||
if (name === token || name.startsWith(token + " ")) matches.push([group, name, args, desc]);
|
||||
const have = canon(name);
|
||||
if (have === want || have.startsWith(want + " ")) matches.push([group, name, args, desc]);
|
||||
}
|
||||
}
|
||||
if (!matches.length) return false;
|
||||
@@ -3606,6 +3623,26 @@ async function cmdRepl() {
|
||||
// prompt. Handlers that need the server throw ServerDownError; the caller
|
||||
// decides whether to fall back to an offline reader or report the refusal.
|
||||
|
||||
/** Command names that are another command under the user-facing word.
|
||||
* `workspace` is the name to use; `lanes` (the handler's own name) and
|
||||
* `--lane` are the OLD names, still working but deprecated. Removing them is
|
||||
* step 4 of docs/plans/workspace-rename/README.md — not before the third
|
||||
* release after the first one that shipped these aliases. */
|
||||
const COMMAND_ALIASES = { workspace: "lanes", workspaces: "lanes" };
|
||||
const canonicalArgv = (argv) =>
|
||||
COMMAND_ALIASES[argv[0]] ? [COMMAND_ALIASES[argv[0]], ...argv.slice(1)] : argv;
|
||||
|
||||
/** One stderr line when an old name is typed, so people and agents move over
|
||||
* before step 4 removes it. stderr only: scripts reading stdout see nothing. */
|
||||
function warnOldNames(argv) {
|
||||
const old = [];
|
||||
if (argv[0] === "lanes") old.push("`agenthub lanes` → `agenthub workspace`");
|
||||
if (argv.includes("--lane")) old.push("`--lane` → `--workspace`");
|
||||
if (old.length) {
|
||||
console.error(c.dim(`note: old name, still works for now — use ${old.join(", ")}`));
|
||||
}
|
||||
}
|
||||
|
||||
/** All top-level command tokens, derived from COMMAND_GROUPS (first word of
|
||||
* each invocation) plus the always-available `help`. Used by the REPL
|
||||
* completer and unknown-command detection so they never drift from help. */
|
||||
@@ -3615,6 +3652,8 @@ const COMMANDS = (() => {
|
||||
for (const [name] of items) set.add(name.split(" ")[0]);
|
||||
}
|
||||
set.add("help");
|
||||
// The old names stay typeable (and completable) for scripts written before.
|
||||
for (const [alias, canonical] of Object.entries(COMMAND_ALIASES)) set.add(alias).add(canonical);
|
||||
return [...set];
|
||||
})();
|
||||
|
||||
@@ -3632,7 +3671,8 @@ const SUBCOMMANDS = {
|
||||
* for usage/validation failures. `cmd === "repl"` is handled by the entry
|
||||
* points, not here, to avoid recursive REPLs. */
|
||||
async function runCommand(argv) {
|
||||
const [cmd, ...rest] = argv;
|
||||
warnOldNames(argv);
|
||||
const [cmd, ...rest] = canonicalArgv(argv);
|
||||
const { flags, positional } = parseArgs(rest);
|
||||
switch (cmd) {
|
||||
case "health":
|
||||
@@ -3702,7 +3742,7 @@ async function runCommand(argv) {
|
||||
if (rest[1] === "init") return cmdLanesProfileInit(rest.slice(2));
|
||||
if (rest[1] === "check") return cmdLanesProfileCheck(rest.slice(2));
|
||||
console.error(
|
||||
"usage: agenthub lanes profile init <repo> [--force] | agenthub lanes profile check [<path>]"
|
||||
"usage: agenthub workspace profile init <repo> [--force] | agenthub workspace profile check [<path>]"
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
@@ -3786,6 +3826,7 @@ async function runCommand(argv) {
|
||||
* an embedding loop can stay alive.
|
||||
*/
|
||||
async function handleServerDown(argv) {
|
||||
argv = canonicalArgv(argv);
|
||||
const cmd = argv[0];
|
||||
const { flags, positional } = parseArgs(argv.slice(1));
|
||||
const handler = OFFLINE_HANDLERS[cmd];
|
||||
|
||||
Reference in New Issue
Block a user