feat(ui): call lanes "workspaces" in the UI, CLI output and docs

Copy-only rename (workspace redesign, phase 0). The word a user reads
changes — "Lane" → "Workspace", "Add lane" → "Add workspace" — in both
locales, in agenthub CLI prose and help, in the server's error messages
and in the docs. Every identifier stays: /api/lanes, `agenthub lanes`,
--lane, AGENTHUB_LANE_ID, error codes, tmux names, DB tables, file,
component and i18n key names. docs/LANES.md keeps its name, retitled
"Workspaces (lanes)".
This commit is contained in:
2026-10-01 16:03:24 +07:00
parent e01cb26bde
commit 5b3884b309
43 changed files with 928 additions and 891 deletions
@@ -243,7 +243,7 @@ describe("AddLaneModal — picking an existing branch (issue #34)", () => {
await user.click(screen.getByRole("button", { name: "Existing branch" }));
const select = await screen.findByLabelText("Branch to check out");
await user.selectOptions(select, "feat/other");
await user.click(screen.getByRole("button", { name: "Add lane" }));
await user.click(screen.getByRole("button", { name: "Add workspace" }));
await waitFor(() =>
expect(api.lanes.worktree).toHaveBeenCalledWith(
@@ -287,7 +287,7 @@ describe("AddLaneModal — picking an existing branch (issue #34)", () => {
"Automatic — named from the title"
);
await user.click(screen.getByRole("button", { name: "Add lane" }));
await user.click(screen.getByRole("button", { name: "Add workspace" }));
await waitFor(() =>
expect(api.lanes.worktree).toHaveBeenCalledWith(
expect.objectContaining({ branch: undefined, base: "main" })
@@ -301,7 +301,7 @@ describe("AddLaneModal — worktree mode (default)", () => {
// the server name it" (issue #34).
it("disables confirm until a repo, a title and a resolved branch list are all present", () => {
renderModal();
expect(screen.getByRole("button", { name: "Add lane" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Add workspace" })).toBeDisabled();
});
it("looks up branches once the repo path settles, and shows them as a picker", async () => {
@@ -337,7 +337,7 @@ describe("AddLaneModal — worktree mode (default)", () => {
await waitFor(() => expect(api.lanes.branches).toHaveBeenCalled());
expect(await screen.findByText(/Not a git repository/)).toBeInTheDocument();
expect(screen.queryByLabelText("Branch to fork from")).toBeNull();
expect(screen.getByRole("button", { name: "Add lane" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Add workspace" })).toBeDisabled();
});
it("refuses a worktree of a repo with no commits and steers to Repo mode", async () => {
@@ -355,11 +355,11 @@ describe("AddLaneModal — worktree mode (default)", () => {
expect(await screen.findByText(/commit first, then publish/i)).toBeInTheDocument();
await user.type(screen.getByLabelText("Title"), "demo");
// There is nothing to branch from, so the worktree cannot be created.
expect(screen.getByRole("button", { name: "Add lane" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Add workspace" })).toBeDisabled();
await user.click(screen.getByRole("button", { name: "Use as a Repo lane" }));
await user.click(screen.getByRole("button", { name: "Use as a Repo workspace" }));
expect(screen.getByRole("button", { name: "Repo" })).toHaveAttribute("aria-pressed", "true");
expect(screen.getByRole("button", { name: "Add lane" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Add workspace" })).toBeEnabled();
});
it("submits through the worktree provisioning endpoint, with the typed branch name", async () => {
@@ -375,7 +375,7 @@ describe("AddLaneModal — worktree mode (default)", () => {
const user = userEvent.setup();
await fillWorktreeForm(user, { title: "New feature", branch: "feat/new-feature" });
await user.click(screen.getByRole("button", { name: "Add lane" }));
await user.click(screen.getByRole("button", { name: "Add workspace" }));
await waitFor(() => {
expect(api.lanes.worktree).toHaveBeenCalledWith({
@@ -407,7 +407,7 @@ describe("AddLaneModal — worktree mode (default)", () => {
const user = userEvent.setup();
await fillWorktreeForm(user, { title: "New feature" });
await user.click(screen.getByRole("button", { name: "Add lane" }));
await user.click(screen.getByRole("button", { name: "Add workspace" }));
expect(await screen.findByText("EWORKTREEDIRCOLLISION")).toBeInTheDocument();
expect(onClose).not.toHaveBeenCalled();
@@ -432,7 +432,7 @@ describe("AddLaneModal — worktree mode (default)", () => {
const user = userEvent.setup();
await fillWorktreeForm(user);
await user.click(screen.getByRole("button", { name: "Add lane" }));
await user.click(screen.getByRole("button", { name: "Add workspace" }));
await waitFor(() => expect(api.lanes.worktree).toHaveBeenCalled());
await waitFor(() => expect(api.lanes.profileInit).toHaveBeenCalledWith(42));
@@ -457,7 +457,7 @@ describe("AddLaneModal — worktree mode (default)", () => {
const user = userEvent.setup();
await fillWorktreeForm(user, { title: "demo2" });
await user.click(screen.getByRole("button", { name: "Add lane" }));
await user.click(screen.getByRole("button", { name: "Add workspace" }));
await waitFor(() => expect(onAdded).toHaveBeenCalled());
expect(onClose).not.toHaveBeenCalled();
@@ -478,14 +478,14 @@ describe("AddLaneModal — worktree mode (default)", () => {
const user = userEvent.setup();
await fillWorktreeForm(user, { title: "demo3" });
await user.click(screen.getByRole("button", { name: "Add lane" }));
await user.click(screen.getByRole("button", { name: "Add workspace" }));
// A failure is the one case worth reading before the dialog goes away.
expect(await screen.findByText("Setup")).toBeInTheDocument();
expect(onClose).not.toHaveBeenCalled();
// And then the primary button must close, not sit there disabled: the lane
// already exists, so "Add lane" has nothing left to do.
expect(screen.queryByRole("button", { name: "Add lane" })).toBeNull();
expect(screen.queryByRole("button", { name: "Add workspace" })).toBeNull();
await user.click(screen.getByRole("button", { name: "Close" }));
expect(onClose).toHaveBeenCalled();
});
@@ -510,7 +510,7 @@ describe("AddLaneModal — worktree mode (default)", () => {
const user = userEvent.setup();
await fillWorktreeForm(user, { title: "demo4" });
await user.click(screen.getByRole("button", { name: "Add lane" }));
await user.click(screen.getByRole("button", { name: "Add workspace" }));
await waitFor(() => expect(onAdded).toHaveBeenCalled());
expect(await screen.findByText(/auto-setup was skipped/)).toBeInTheDocument();
@@ -525,12 +525,12 @@ describe("AddLaneModal — repo mode (adopt)", () => {
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: "Repo" }));
expect(screen.getByRole("button", { name: "Add lane" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Add workspace" })).toBeDisabled();
const dirField = screen.getByLabelText("Directory");
await focusField(user, dirField);
await user.type(dirField, "/Users/tester/projects/repo");
expect(screen.getByRole("button", { name: "Add lane" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Add workspace" })).toBeEnabled();
expect(screen.queryByLabelText("Branch to fork from")).toBeNull();
expect(screen.queryByLabelText("New branch name")).toBeNull();
expect(api.lanes.branches).not.toHaveBeenCalled();
@@ -551,7 +551,7 @@ describe("AddLaneModal — repo mode (adopt)", () => {
await focusField(user, dirField);
await user.type(dirField, "/Users/tester/projects/repo");
await user.type(screen.getByLabelText("Title"), "main repo");
await user.click(screen.getByRole("button", { name: "Add lane" }));
await user.click(screen.getByRole("button", { name: "Add workspace" }));
await waitFor(() =>
expect(api.lanes.ensure).toHaveBeenCalledWith({
@@ -582,9 +582,11 @@ describe("AddLaneModal — repo mode (adopt)", () => {
const dirField = screen.getByLabelText("Directory");
await focusField(user, dirField);
await user.type(dirField, "/Users/tester/projects/repo");
await user.click(screen.getByRole("button", { name: "Add lane" }));
await user.click(screen.getByRole("button", { name: "Add workspace" }));
expect(await screen.findByText(/already has a lane.*#3.*SG - Services/)).toBeInTheDocument();
expect(
await screen.findByText(/already has a workspace.*#3.*SG - Services/)
).toBeInTheDocument();
// The existing lane is selected so the user sees it, but nothing claims a new one.
expect(onAdded).toHaveBeenCalledWith(expect.objectContaining({ id: 3 }));
expect(onClose).not.toHaveBeenCalled();
@@ -625,7 +627,7 @@ describe("AddLaneModal — folder browse", () => {
expect(screen.queryByRole("dialog", { name: "Browse for a folder" })).toBeNull();
expect(
screen.getByRole("dialog", { name: "Create a lane from a working directory" })
screen.getByRole("dialog", { name: "Create a workspace from a working directory" })
).toBeInTheDocument();
expect(onClose).not.toHaveBeenCalled();
});
@@ -655,7 +657,7 @@ describe("AddLaneModal — AI Router", () => {
const repoField = screen.getByLabelText("Directory");
await focusField(user, repoField);
await user.type(repoField, "/Users/tester/projects/repo");
await user.click(screen.getByRole("button", { name: "Add lane" }));
await user.click(screen.getByRole("button", { name: "Add workspace" }));
await waitFor(() => expect(api.lanes.routerApply).toHaveBeenCalledWith(9, "native"));
});
@@ -674,7 +676,7 @@ describe("AddLaneModal — AI Router", () => {
await focusField(user, repoField);
await user.type(repoField, "/Users/tester/projects/repo");
await user.click(await screen.findByRole("radio", { name: "AI Router" }));
await user.click(screen.getByRole("button", { name: "Add lane" }));
await user.click(screen.getByRole("button", { name: "Add workspace" }));
await waitFor(() => expect(api.lanes.routerApply).toHaveBeenCalledWith(9, "9router"));
});
@@ -703,7 +705,7 @@ describe("AddLaneModal — AI Router", () => {
const repoField = screen.getByLabelText("Directory");
await focusField(user, repoField);
await user.type(repoField, "/Users/tester/projects/repo");
await user.click(screen.getByRole("button", { name: "Add lane" }));
await user.click(screen.getByRole("button", { name: "Add workspace" }));
await waitFor(() => expect(api.lanes.ensure).toHaveBeenCalled());
});
@@ -726,7 +728,7 @@ describe("AddLaneModal — a created lane is never lost to a setup failure", ()
const repoField = screen.getByLabelText("Directory");
await focusField(user, repoField);
await user.type(repoField, "/Users/tester/projects/repo");
await user.click(screen.getByRole("button", { name: "Add lane" }));
await user.click(screen.getByRole("button", { name: "Add workspace" }));
await waitFor(() => expect(onAdded).toHaveBeenCalled());
// …and the failure is still surfaced, on a modal left open.
@@ -792,7 +794,7 @@ describe("AddLaneModal — tool selection", () => {
const picker = await screen.findByRole("button", { name: /Claude Code.*Default/ });
expect(picker).toHaveAttribute("aria-expanded", "false");
await user.click(picker);
const list = screen.getByRole("group", { name: /Which CLI drives this lane/ });
const list = screen.getByRole("group", { name: /Which CLI drives this workspace/ });
expect(
within(list)
.getAllByRole("button")
@@ -802,7 +804,7 @@ describe("AddLaneModal — tool selection", () => {
// The picked tool replaces the default on the button, and the list closes.
expect(screen.getByRole("button", { name: "Codex", expanded: false })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Gemini" })).toBeNull();
await user.click(screen.getByRole("button", { name: /add lane|create/i }));
await user.click(screen.getByRole("button", { name: /add workspace|create/i }));
await waitFor(() =>
expect(api.lanes.ensure).toHaveBeenCalledWith(expect.objectContaining({ tool: "codex" }))
@@ -842,7 +844,7 @@ describe("AddLaneModal — tool selection", () => {
const dirField = screen.getByLabelText("Directory");
await focusField(user, dirField);
await user.type(dirField, "/tmp/demo");
await user.click(screen.getByRole("button", { name: /add lane|create/i }));
await user.click(screen.getByRole("button", { name: /add workspace|create/i }));
await waitFor(() =>
expect(api.lanes.ensure).toHaveBeenCalledWith(expect.objectContaining({ tool: "claude" }))
@@ -135,7 +135,7 @@ describe("DestructiveLaneModal", () => {
onConfirm={onConfirm}
/>
);
const confirmButton = await screen.findByRole("button", { name: "Delete lane record" });
const confirmButton = await screen.findByRole("button", { name: "Delete workspace record" });
await waitFor(() => expect(confirmButton).not.toBeDisabled());
// "adopted" is shown as context, not as a refusal.
expect(screen.getByText(/Only the dashboard's record of it is dropped/)).toBeInTheDocument();
@@ -167,7 +167,7 @@ describe("DestructiveLaneModal", () => {
onConfirm={onConfirm}
/>
);
const confirmButton = await screen.findByRole("button", { name: "Delete lane record" });
const confirmButton = await screen.findByRole("button", { name: "Delete workspace record" });
await waitFor(() => expect(confirmButton).not.toBeDisabled());
expect(screen.queryByRole("checkbox")).not.toBeInTheDocument();
fireEvent.click(confirmButton);
@@ -212,9 +212,9 @@ describe("DestructiveLaneModal", () => {
onConfirm={onConfirm}
/>
);
const confirmButton = await screen.findByRole("button", { name: "Delete lane record" });
const confirmButton = await screen.findByRole("button", { name: "Delete workspace record" });
await waitFor(() => expect(confirmButton).not.toBeDisabled());
expect(screen.getByText(/The lane directory is already gone/)).toBeInTheDocument();
expect(screen.getByText(/The workspace directory is already gone/)).toBeInTheDocument();
fireEvent.click(confirmButton);
expect(onConfirm).toHaveBeenCalledWith({
expect: { head: null, dirty: 0, untracked: 0, unpushed: 0 },
@@ -259,7 +259,7 @@ describe("DestructiveLaneModal", () => {
onConfirm={onConfirm}
/>
);
const confirmButton = await screen.findByRole("button", { name: "Delete lane record" });
const confirmButton = await screen.findByRole("button", { name: "Delete workspace record" });
await waitFor(() => expect(confirmButton).not.toBeDisabled());
expect(screen.getByText(/cannot be read as a Git worktree/)).toBeInTheDocument();
fireEvent.click(confirmButton);
@@ -289,7 +289,7 @@ describe("DestructiveLaneModal", () => {
onConfirm={onConfirm}
/>
);
const confirmButton = await screen.findByRole("button", { name: "Delete lane record" });
const confirmButton = await screen.findByRole("button", { name: "Delete workspace record" });
await waitFor(() => expect(confirmButton).not.toBeDisabled());
expect(screen.queryByRole("checkbox")).not.toBeInTheDocument();
fireEvent.click(confirmButton);
@@ -207,7 +207,7 @@ describe("LaneChangesPanel", () => {
});
render(<LaneChangesPanel laneId={7} />);
expect(
await screen.findByText("This lane's directory is not a readable git repository.")
await screen.findByText("This workspace's directory is not a readable git repository.")
).toBeInTheDocument();
});
@@ -197,7 +197,7 @@ describe("LaneGitBar", () => {
);
await user.selectOptions(screen.getByLabelText("Switch branch…"), "development");
await user.click(screen.getByRole("button", { name: "New lane here" }));
await user.click(screen.getByRole("button", { name: "New workspace here" }));
expect(onNewLane).toHaveBeenCalledWith("development");
});
@@ -215,7 +215,7 @@ describe("LaneGitBar", () => {
render(<LaneGitBar laneId={3} git={facts()} onChanged={vi.fn()} onNewLane={vi.fn()} />);
await user.click(screen.getByRole("button", { name: /Branch/ }));
const select = await screen.findByLabelText("Switch branch…");
const newLane = () => screen.getByRole("button", { name: "New lane here" });
const newLane = () => screen.getByRole("button", { name: "New workspace here" });
// This lane's own branch: its worktree is this lane.
await user.selectOptions(select, "feat/thing");
@@ -287,7 +287,7 @@ describe("LaneGitBar", () => {
worktrees: [{ path: "/repo/dev", branch: "development" }],
});
await user.selectOptions(select, "development");
const newLane = screen.getByRole("button", { name: "New lane here" });
const newLane = screen.getByRole("button", { name: "New workspace here" });
expect(newLane).toBeEnabled();
expect(newLane).not.toHaveAttribute("title", expect.stringContaining("/repo/dev"));
});
@@ -425,7 +425,7 @@ export function LaneConsolePane({
const startedId = laneStartResult.run_id ?? laneStartResult.lane?.run_id;
if (!startedId) {
throw new Error("No run_id returned from lane start");
throw new Error(t("errors.noRunIdReturned"));
}
fetched = await api.run.get(startedId);
+45 -45
View File
@@ -1,11 +1,11 @@
{
"action.clear": "clear state",
"action.forget": "Delete lane",
"action.forget": "Delete workspace",
"action.purge": "delete history",
"action.reset": "reset worktree",
"action.start": "start",
"action.stop": "stop",
"actionError": "Lane action failed: {{message}}",
"actionError": "Workspace action failed: {{message}}",
"actionErrorUnknown": "Unknown error",
"actions.busy": "…",
"actions.mcpSync": "Sync MCP",
@@ -15,10 +15,10 @@
"actions.syncCheckCollision": "Migration collision: {{file}} → rename to {{suggestion}}",
"actions.tools": "Tools",
"actions.writeDocs": "Create/update readme file",
"actions.writeDocsHint": "Opens this lane's terminal and runs /sg-all:sg-md-writing there: creates CLAUDE.md/AGENTS.md if missing, or updates them concisely if they already exist.",
"actions.writeDocsResult": "Started — see this lane's terminal.",
"add": "Add lane",
"addLane": "Create a lane from a working directory",
"actions.writeDocsHint": "Opens this workspace's terminal and runs /sg-all:sg-md-writing there: creates CLAUDE.md/AGENTS.md if missing, or updates them concisely if they already exist.",
"actions.writeDocsResult": "Started — see this workspace's terminal.",
"add": "Add workspace",
"addLane": "Create a workspace from a working directory",
"addLaneBaseLabel": "Branch to fork from",
"addLaneBranchBusy": "{{branch}} — already checked out at {{path}}",
"addLaneBranchExistingHint": "The worktree checks this branch out as it is — no new branch, no base.",
@@ -30,19 +30,19 @@
"addLaneBranchModeExisting": "Existing branch",
"addLaneBranchModeNew": "New branch",
"addLaneBranchPlaceholder": "Automatic — named from the title",
"addLaneNoBranches": "This repo has no commits yet, so a worktree has nothing to branch from. Add it as a Repo lane: commit first, then publish it from the Changes panel.",
"addLaneNoBranchesUseRepo": "Use as a Repo lane",
"addLaneNoBranches": "This repo has no commits yet, so a worktree has nothing to branch from. Add it as a Repo workspace: commit first, then publish it from the Changes panel.",
"addLaneNoBranchesUseRepo": "Use as a Repo workspace",
"addLaneNoFreeBranches": "Every branch in this repo is already checked out in a worktree. Create a new branch instead.",
"addLaneExists": "This folder already has a lane: #{{id}} {{title}}. It is selected; pick another folder to add a new one.",
"addLaneExists": "This folder already has a workspace: #{{id}} {{title}}. It is selected; pick another folder to add a new one.",
"addLaneNotARepo": "Not a git repository (or no read access) yet.",
"addLaneProvisionFailed": "Worktree provisioning failed or timed out — the lane exists but auto-setup was skipped. Check the lane's git facts, then run agents/mcp setup manually.",
"addLaneRepoHint": "An existing git repo. The dashboard creates a new worktree for the lane, not a folder you pick.",
"addLaneProvisionFailed": "Worktree provisioning failed or timed out — the workspace exists but auto-setup was skipped. Check the workspace's git facts, then run agents/mcp setup manually.",
"addLaneRepoHint": "An existing git repo. The dashboard creates a new worktree for the workspace, not a folder you pick.",
"addLaneRepoHintAdopt": "An existing directory. The dashboard tracks it as-is — no worktree, no new branch.",
"addLaneRepoLabel": "Source repository",
"addLaneRepoLabelAdopt": "Directory",
"addLaneRouterCustom": "AI Router",
"addLaneRouterEmptyHint": "No router profile saved yet — add one under Settings › AI Router.",
"addLaneRouterHint": "Native talks to the vendor's own API. An AI Router profile is written into this lane's own config when it is created.",
"addLaneRouterHint": "Native talks to the vendor's own API. An AI Router profile is written into this workspace's own config when it is created.",
"addLaneRouterLabel": "Model endpoint",
"addLaneRouterLoading": "Loading router profiles…",
"addLaneRouterProfileLabel": "Router profile",
@@ -57,7 +57,7 @@
"addLaneNoTools": "No AI tool is installed and enabled.",
"addLaneNoToolsLink": "Open Settings → AI Tools",
"addLaneRouterNativeTool": "Native ({{tool}})",
"addLaneToolHint": "Which CLI drives this lane. Cannot be changed later.",
"addLaneToolHint": "Which CLI drives this workspace. Cannot be changed later.",
"addLaneWorktreeDetached": "(detached)",
"addLaneWorktreesMore": "+{{count}} more",
"addLaneWorktreesTitle": "Worktrees of this repo ({{count}})",
@@ -101,7 +101,7 @@
"changes.openFile": "Open {{path}}",
"changes.quickOpen": "Find a file by path…",
"changes.range.branch": "vs base",
"changes.range.branchHint": "base...HEAD — everything this lane's branch adds on top of its base",
"changes.range.branchHint": "base...HEAD — everything this workspace's branch adds on top of its base",
"changes.range.work": "Uncommitted",
"changes.range.workHint": "Working tree vs HEAD — what has been touched and not committed",
"changes.refresh": "Refresh",
@@ -117,20 +117,20 @@
"changes.stage": "Stage this file",
"changes.title": "Changes",
"changes.truncated": "Some changes were left out to keep this panel responsive.",
"changes.unavailable": "This lane's directory is not a readable git repository.",
"changes.unavailable": "This workspace's directory is not a readable git repository.",
"changes.unstage": "Unstage this file",
"changes.wholeFile": "Whole file",
"confirmRemoveCancel": "Cancel",
"confirmRemoveConfirm": "Remove",
"confirmRemoveMessage": "This action cannot be undone.",
"confirmRemoveTitle": "Remove lane?",
"confirmRemoveTitle": "Remove workspace?",
"countDead": "dead",
"countNeedsYou": "need you",
"countRunning": "running",
"countTotal": "lanes",
"countTotal": "workspaces",
"destructive.blocked.adopted": "This is an adopted directory and cannot be changed by a worktree action.",
"destructive.blocked.missing": "The lane directory is missing, so this action cannot continue.",
"destructive.blocked.unreadable": "The lane directory cannot be read as a Git worktree, so this action cannot continue.",
"destructive.blocked.missing": "The workspace directory is missing, so this action cannot continue.",
"destructive.blocked.unreadable": "The workspace directory cannot be read as a Git worktree, so this action cannot continue.",
"destructive.cancel": "Cancel",
"destructive.count.bytesEstimate": "Estimated space freed",
"destructive.count.dirty": "Modified files",
@@ -142,26 +142,26 @@
"destructive.count.untracked": "Untracked files",
"destructive.force": "I understand these unpushed commits will be discarded.",
"destructive.loading": "Loading current facts…",
"destructive.notice.activeSessionSkipped": "One or more sessions in this lane are still active and will be kept, not purged.",
"destructive.notice.adopted": "This lane points at a directory you own. Only the dashboard's record of it is dropped — the directory and its files are left untouched.",
"destructive.notice.missing": "The lane directory is already gone. Deleting the lane drops the dashboard's record only; nothing on disk and no Git record is touched.",
"destructive.notice.unreadable": "The lane directory cannot be read as a Git worktree. That changes nothing here: only the dashboard's record is deleted, the directory is never touched.",
"destructive.notice.activeSessionSkipped": "One or more sessions in this workspace are still active and will be kept, not purged.",
"destructive.notice.adopted": "This workspace points at a directory you own. Only the dashboard's record of it is dropped — the directory and its files are left untouched.",
"destructive.notice.missing": "The workspace directory is already gone. Deleting the workspace drops the dashboard's record only; nothing on disk and no Git record is touched.",
"destructive.notice.unreadable": "The workspace directory cannot be read as a Git worktree. That changes nothing here: only the dashboard's record is deleted, the directory is never touched.",
"destructive.purge.confirm": "Purge history",
"destructive.purge.message": "Approve the current counts before deleting stored session history.",
"destructive.purge.title": "Purge lane history?",
"destructive.remove.confirm": "Delete lane record",
"destructive.remove.message": "Only the dashboard's record of this lane is deleted. Its working tree, its branch and its database are left exactly as they are — delete them yourself if you want them gone.",
"destructive.remove.title": "Delete lane record?",
"destructive.purge.title": "Purge workspace history?",
"destructive.remove.confirm": "Delete workspace record",
"destructive.remove.message": "Only the dashboard's record of this workspace is deleted. Its working tree, its branch and its database are left exactly as they are — delete them yourself if you want them gone.",
"destructive.remove.title": "Delete workspace record?",
"destructive.reset.confirm": "Reset worktree",
"destructive.reset.message": "Approve the current counts before resetting this worktree.",
"destructive.reset.survives": "Gitignored files such as node_modules and .env are kept; untracked files are deleted.",
"destructive.reset.title": "Reset managed worktree?",
"destructive.warning.no-remote": "No Git remote is configured, so nothing here is backed up remotely. You can still proceed.",
"empty.body": "A lane is one working directory the dashboard follows — its pipeline, sessions and terminal. Click anywhere on this card to add one, or from a terminal:",
"empty.title": "No lanes yet",
"empty.body": "A workspace is one working directory the dashboard follows — its pipeline, sessions and terminal. Click anywhere on this card to add one, or from a terminal:",
"empty.title": "No workspaces yet",
"features.archived": "archived",
"features.live": "Live",
"features.viewingArchived": "Viewing archived feature \"{{slug}}\" — the lane keeps running; this is a read-only snapshot.",
"features.viewingArchived": "Viewing archived feature \"{{slug}}\" — the workspace keeps running; this is a read-only snapshot.",
"git.filesHeading_one": "{{count}} file changed",
"git.filesHeading_other": "{{count}} files changed",
"git.filesMore_one": "… and {{count}} more file",
@@ -177,9 +177,9 @@
"gitBar.fetchHint": "git fetch --all --prune",
"gitBar.errNoRemote": "No remote to push to or pull from. Add one, or set this branch's upstream, in a terminal.",
"gitBar.inSync": "in sync",
"gitBar.newLane": "New lane here",
"gitBar.newLane": "New workspace here",
"gitBar.newLaneHeld": "Already checked out at {{path}} — git allows one worktree per branch",
"gitBar.newLaneHint": "Open Add lane for a sibling worktree on the selected branch",
"gitBar.newLaneHint": "Open Add workspace for a sibling worktree on the selected branch",
"gitBar.noCommits": "no commits yet",
"gitBar.noUpstream": "not pushed",
"gitBar.publish": "Publish…",
@@ -202,25 +202,25 @@
"kind.adopted": "adopted",
"kind.managed": "managed",
"laneDetail.hide": "Hide details",
"laneDetail.show": "Lane details",
"laneDetail.show": "Workspace details",
"locks.held_one": "{{count}} lock held",
"locks.held_other": "{{count}} locks held",
"mode.repo": "Repo",
"mode.repoHint": "Adopt this directory as a lane, as-is — no worktree, no new branch.",
"mode.repoHint": "Adopt this directory as a workspace, as-is — no worktree, no new branch.",
"mode.worktree": "Worktree",
"mode.worktreeHint": "Provision a new git worktree + branch from a source repo.",
"moreActions": "More actions",
"pipelinePicker.label": "Pipeline template",
"pipelinePicker.stageMismatch": "Stage \"{{stage}}\" matches no node in \"{{pipeline}}\" — declare one of: {{nodes}}",
"preflightError": "Could not load the current lane facts.",
"preflightErrorWithMessage": "Could not load the current lane facts: {{message}}",
"preflightError": "Could not load the current workspace facts.",
"preflightErrorWithMessage": "Could not load the current workspace facts: {{message}}",
"proof.ticketReport": "Task report",
"renameLabel": "Rename this lane",
"renameLabel": "Rename this workspace",
"reorderHint": "Drag to reorder · Alt+←/→",
"router.applied": "Applied.",
"router.native": "Native ({{tool}})",
"router.nativeOnly": "{{tool}} runs on its own settings — AI Router profiles work with Claude Code and Codex only.",
"router.appliedRestart": "Applied, but the session running in this lane still uses the old models — restart it to take effect.",
"router.appliedRestart": "Applied, but the session running in this workspace still uses the old models — restart it to take effect.",
"router.label": "Model",
"router.restart": "Restart",
"router.restarted": "Session restarted with the new router.",
@@ -233,17 +233,17 @@
"runtime.slot": "slot {{slot}}",
"runtime.steppedAsideTitle": "Stepped aside from {{expected}}, which was already in use",
"runtime.stop": "■ down",
"runtime.toggleTitle": "Boot or stop this lane's own application stack (separate from its Claude run)",
"splitView.emptyPane": "No lane selected for this pane.",
"runtime.toggleTitle": "Boot or stop this workspace's own application stack (separate from its Claude run)",
"splitView.emptyPane": "No workspace selected for this pane.",
"splitView.paneCount": "{{count}} pane",
"splitView.paneLaneLabel": "Pane lane selector",
"splitView.pickLane": "Pick a lane",
"splitView.paneLaneLabel": "Pane workspace selector",
"splitView.pickLane": "Pick a workspace",
"status.failed": "failed",
"status.idle": "idle",
"status.provisioning": "provisioning",
"status.running": "running",
"statusDead": "DEAD",
"title": "Lanes",
"title": "Workspaces",
"tooltipStart": "Spawn a conversation-mode run with no initial prompt; driven from CLI or via message",
"worktrees.heading_one": "{{count}} worktree",
"worktrees.heading_other": "{{count}} worktrees",
@@ -276,10 +276,10 @@
"gitBar.loginSubmit": "Log in and retry",
"gitBar.loginCancel": "Cancel",
"gitBar.merge": "Merge into current",
"gitBar.mergeHint": "Merge the selected branch into the branch this lane is on",
"gitBar.mergeHint": "Merge the selected branch into the branch this workspace is on",
"gitBar.mergeUpstream": "Merge {{upstream}}",
"gitBar.mergeUpstreamHint": "The branch has diverged from its upstream: merge it in (conflicts are resolved in the Changes panel)",
"gitBar.mergeConfirm": "Merge {{branch}} into {{current}}? Files in this lane change now — if an agent is working in this lane, let it finish first.",
"gitBar.mergeConfirm": "Merge {{branch}} into {{current}}? Files in this workspace change now — if an agent is working in this workspace, let it finish first.",
"gitBar.mergeConflicts": "Merge stopped on {{count}} conflicted file(s) — resolve them in the list below.",
"gitBar.merging": "A merge is in progress: finish or abort it first",
"changes.conflictsGroup": "Merge conflicts",
+4 -4
View File
@@ -34,12 +34,12 @@
},
"errors": {
"attachFailed": "Couldn't attach to run: {{message}}",
"attachedToLiveInstead": "This lane's previous session is still running, so nothing new could start — attached to it instead. Kill it from Active runs to start fresh.",
"attachedToLiveInstead": "This workspace's previous session is still running, so nothing new could start — attached to it instead. Kill it from Active runs to start fresh.",
"cwdRequired": "Working directory required to start a run",
"killFailed": "Couldn't stop: {{message}}",
"laneCreateFailed": "Failed to create lane: {{message}}",
"noLaneSelected": "No lane selected or could not create one",
"noRunIdReturned": "No run_id returned from lane start",
"laneCreateFailed": "Failed to create workspace: {{message}}",
"noLaneSelected": "No workspace selected or could not create one",
"noRunIdReturned": "No run_id returned from workspace start",
"runStartedButNotAttached": "Run is live but couldn't attach to console: {{message}}",
"sendFailed": "Couldn't send: {{message}}",
"startFailed": "Couldn't start run: {{message}}"
+14 -14
View File
@@ -68,7 +68,7 @@
"title": "CLAUDE_HOME",
"description": "Root directory for Claude Code session data. Changing this updates the scan path for transcripts and imports immediately.",
"placeholder": "/path/to/claude-home",
"routersMissing": "Saved. These lanes name an AI Router profile the new home does not have, and start on Native until it is added there or they are re-pointed: {{lanes}}"
"routersMissing": "Saved. These workspaces name an AI Router profile the new home does not have, and start on Native until it is added there or they are re-pointed: {{lanes}}"
},
"hooks": {
"title": "Hook Configuration",
@@ -80,8 +80,8 @@
"failed": "Hook installation failed"
},
"lanesSection": {
"title": "Lanes",
"description": "Machine-wide lane housekeeping.",
"title": "Workspaces",
"description": "Machine-wide workspace housekeeping.",
"runHousekeeping": "Run housekeeping",
"dryRun": "Dry run",
"gcResult": "Reaped {{reaped}} process(es), capped {{capped}} log(s)"
@@ -474,7 +474,7 @@
},
"aiRouter": {
"title": "AI Router",
"description": "Endpoints that stand in front of Claude Code and Codex — 9Router, OpenRouter, a self-hosted gateway. Save one here, then pick it when you create a lane.",
"description": "Endpoints that stand in front of Claude Code and Codex — 9Router, OpenRouter, a self-hosted gateway. Save one here, then pick it when you create a workspace.",
"add": "Add profile",
"edit": "Edit",
"remove": "Remove",
@@ -487,7 +487,7 @@
"keySet": "Saved",
"keyMissing": "None",
"fieldLabel": "Name",
"fieldLabelHint": "What you will see in the lane form. Free text.",
"fieldLabelHint": "What you will see in the workspace form. Free text.",
"fieldBaseUrl": "Base URL",
"fieldBaseUrlHint": "The router's OpenAI/Anthropic-compatible endpoint, e.g. http://localhost:20128/v1",
"fieldApiKey": "API key",
@@ -506,9 +506,9 @@
},
"save": "Save",
"cancel": "Cancel",
"removeConfirm": "Remove the profile \"{{label}}\"? Every lane using it is reset to the vendor's own API right away — on disk and in the dashboard.",
"removeConfirm": "Remove the profile \"{{label}}\"? Every workspace using it is reset to the vendor's own API right away — on disk and in the dashboard.",
"loading": "Loading profiles…",
"lanesFailed": "Saved, but {{count}} lane could not be updated and still carries the old endpoint: {{lanes}}",
"lanesFailed": "Saved, but {{count}} workspace could not be updated and still carries the old endpoint: {{lanes}}",
"test": "Test",
"testing": "Testing…",
"testOk": "The endpoint answered — {{model}} is reachable with this key.",
@@ -516,7 +516,7 @@
},
"aiTools": {
"title": "AI Tools",
"description": "AI coding CLIs AgentHub can run in a lane. Detected on this machine's PATH; a disabled tool is hidden from Add lane.",
"description": "AI coding CLIs AgentHub can run in a workspace. Detected on this machine's PATH; a disabled tool is hidden from Add workspace.",
"installed": "Installed ({{count}} detected)",
"available": "Available to install ({{count}})",
"refresh": "Refresh",
@@ -585,14 +585,14 @@
},
"codexHome": {
"title": "CODEX_HOME",
"description": "Root directory for Codex config and session journals. The dashboard reads it, writes AI Router layers into it, and starts every codex lane with it.",
"restartNote": "A codex lane already running keeps the home it started with, and shows as idle, until it is restarted.",
"description": "Root directory for Codex config and session journals. The dashboard reads it, writes AI Router layers into it, and starts every codex workspace with it.",
"restartNote": "A codex workspace already running keeps the home it started with, and shows as idle, until it is restarted.",
"placeholder": "/path/to/codex-home",
"routersFailed": "Saved, but the AI Router layer could not be re-written for: {{lanes}}"
},
"codexHooks": {
"title": "Codex hooks",
"description": "Let Codex report to AgentHub as it works: each tool call in the session's timeline, \"Codex needs your permission\" on the lane card and as a notification, and running/idle the moment a turn starts or ends. Tokens and cost come from Codex's journals either way.",
"description": "Let Codex report to AgentHub as it works: each tool call in the session's timeline, \"Codex needs your permission\" on the workspace card and as a notification, and running/idle the moment a turn starts or ends. Tokens and cost come from Codex's journals either way.",
"notInstalled": "Not installed",
"untrusted": "Installed — waiting for your trust in Codex",
"trusted": "Installed and trusted",
@@ -614,7 +614,7 @@
"notInstalled": "Not installed",
"installed": "Installed",
"disabled": "Installed but switched off in hooks.json",
"note": "agy loads hooks at its next start and asks for no trust. The file is global, so the Antigravity IDE runs the hook too; AgentHub ignores what the IDE sends. agy has no hook for a permission prompt, so an Antigravity lane cannot show \"needs your permission\".",
"note": "agy loads hooks at its next start and asks for no trust. The file is global, so the Antigravity IDE runs the hook too; AgentHub ignores what the IDE sends. agy has no hook for a permission prompt, so an Antigravity workspace cannot show \"needs your permission\".",
"install": "Install",
"reinstall": "Reinstall",
"refresh": "Refresh",
@@ -629,9 +629,9 @@
"update": "Update…",
"usage_one": "{{count}} session · ${{cost}}",
"usage_other": "{{count}} sessions · ${{cost}}",
"lanes": "Lanes:",
"lanes": "Workspaces:",
"defaultModel": "Default model",
"defaultModelHint": "The model a new run in this tool's lanes starts on. The run form can still pick another one.",
"defaultModelHint": "The model a new run in this tool's workspaces starts on. The run form can still pick another one.",
"saveDefaultModel": "Save default model",
"config": "Config files",
"configHint": "This tool's own settings files. A save is checked for valid syntax, the old text is backed up in AgentHub's data folder, and nothing is saved if the file changed since you opened it.",
+6 -6
View File
@@ -64,7 +64,7 @@
"why": "When an aggregate chart flags a suspicious session, this is where you go to read exactly what happened, in order."
},
"lanes": {
"what": "Every stage record across all your lanes and their archived features, counted per pipeline node: how many times a stage was entered, passed, and how many of those passes left evidence behind.",
"what": "Every stage record across all your workspaces and their archived features, counted per pipeline node: how many times a stage was entered, passed, and how many of those passes left evidence behind.",
"howToRead": "One row per node. The bar is scaled against the busiest node, split into green (passed with evidence), amber (passed but only claimed) and red (failed). The number on the right is passes / entries and the pass rate.",
"why": "A node with a long amber bar is a stage everyone declares and nobody proves; a node that is never reached is a stage your pipeline does not really use."
},
@@ -233,11 +233,11 @@
"sortedByCompaction": " session(s) (sorted by compaction count)"
},
"lanes": {
"title": "Lane Pipeline Pass Rates",
"title": "Workspace Pipeline Pass Rates",
"subtitle": "How often each stage of your own pipelines is actually passed",
"pipelineLabel": "Pipeline template",
"laneLabel": "Lane",
"allLanes": "All lanes",
"laneLabel": "Workspace",
"allLanes": "All workspaces",
"runs_one": "{{count}} run recorded",
"runs_other": "{{count}} runs recorded",
"legendEvidence": "passed with evidence",
@@ -245,8 +245,8 @@
"legendFailed": "failed",
"never": "never reached",
"copyCommand": "Copy: {{command}}",
"empty": "No lane has recorded a stage yet. Create one with",
"emptyLane": "This lane has not recorded a stage yet.",
"empty": "No workspace has recorded a stage yet. Create one with",
"emptyLane": "This workspace has not recorded a stage yet.",
"legendDetected": "reached, not declared",
"detectedOnly": "detected ×{{count}}"
},
+45 -45
View File
@@ -1,11 +1,11 @@
{
"action.clear": "Dọn trạng thái",
"action.forget": "Xóa lane",
"action.forget": "Xóa workspace",
"action.purge": "Xóa lịch sử",
"action.reset": "Đặt lại worktree",
"action.start": "Bắt đầu",
"action.stop": "Dừng",
"actionError": "Thao tác làn đường thất bại: {{message}}",
"actionError": "Thao tác workspace thất bại: {{message}}",
"actionErrorUnknown": "Lỗi không xác định",
"actions.busy": "…",
"actions.mcpSync": "Đồng bộ MCP",
@@ -15,10 +15,10 @@
"actions.syncCheckCollision": "Trùng migration: {{file}} → đổi tên thành {{suggestion}}",
"actions.tools": "Công cụ",
"actions.writeDocs": "Tạo/cập nhật file readme",
"actions.writeDocsHint": "Mở terminal của lane này và chạy skill /sg-all:sg-md-writing: tạo CLAUDE.md/AGENTS.md nếu chưa có, hoặc cập nhật ngắn gọn nếu đã có.",
"actions.writeDocsResult": "Đã chạy — xem terminal của lane này.",
"add": "Thêm lane",
"addLane": "Tạo lane từ một thư mục làm việc",
"actions.writeDocsHint": "Mở terminal của workspace này và chạy skill /sg-all:sg-md-writing: tạo CLAUDE.md/AGENTS.md nếu chưa có, hoặc cập nhật ngắn gọn nếu đã có.",
"actions.writeDocsResult": "Đã chạy — xem terminal của workspace này.",
"add": "Thêm workspace",
"addLane": "Tạo workspace từ một thư mục làm việc",
"addLaneBaseLabel": "Nhánh để tạo nhánh mới",
"addLaneBranchBusy": "{{branch}} — đang được checkout tại {{path}}",
"addLaneBranchExistingHint": "Worktree checkout đúng nhánh này — không tạo nhánh mới, không cần nhánh gốc.",
@@ -30,19 +30,19 @@
"addLaneBranchModeExisting": "Nhánh có sẵn",
"addLaneBranchModeNew": "Nhánh mới",
"addLaneBranchPlaceholder": "Tự động — đặt theo tiêu đề",
"addLaneNoBranches": "Repo này chưa có commit nào nên worktree không có gì để rẽ nhánh. Hãy thêm nó thành lane Repo: commit trước, rồi publish từ bảng Changes.",
"addLaneNoBranchesUseRepo": "Dùng làm lane Repo",
"addLaneNoBranches": "Repo này chưa có commit nào nên worktree không có gì để rẽ nhánh. Hãy thêm nó thành workspace Repo: commit trước, rồi publish từ bảng Changes.",
"addLaneNoBranchesUseRepo": "Dùng làm workspace Repo",
"addLaneNoFreeBranches": "Mọi nhánh của repo này đều đang nằm trong một worktree. Hãy tạo nhánh mới.",
"addLaneExists": "Thư mục này đã có lane: #{{id}} {{title}}. Lane đó đã được chọn; hãy chọn thư mục khác để thêm lane mới.",
"addLaneExists": "Thư mục này đã có workspace: #{{id}} {{title}}. Workspace đó đã được chọn; hãy chọn thư mục khác để thêm workspace mới.",
"addLaneNotARepo": "Chưa phải repo git (hoặc không có quyền đọc).",
"addLaneProvisionFailed": "Tạo worktree thất bại hoặc quá thời gian chờ — lane vẫn tồn tại nhưng tự động thiết lập đã bị bỏ qua. Kiểm tra git facts của lane, rồi chạy thiết lập agents/mcp thủ công.",
"addLaneRepoHint": "Một repo git có sẵn. Dashboard tự tạo worktree mới cho lane, không phải thư mục bạn chọn.",
"addLaneProvisionFailed": "Tạo worktree thất bại hoặc quá thời gian chờ — workspace vẫn tồn tại nhưng tự động thiết lập đã bị bỏ qua. Kiểm tra git facts của workspace, rồi chạy thiết lập agents/mcp thủ công.",
"addLaneRepoHint": "Một repo git có sẵn. Dashboard tự tạo worktree mới cho workspace, không phải thư mục bạn chọn.",
"addLaneRepoHintAdopt": "Một thư mục có sẵn. Dashboard theo dõi nguyên trạng — không tạo worktree, không tạo nhánh mới.",
"addLaneRepoLabel": "Repo nguồn",
"addLaneRepoLabelAdopt": "Thư mục",
"addLaneRouterCustom": "AI Router",
"addLaneRouterEmptyHint": "Chưa lưu profile router nào — thêm ở Settings › AI Router.",
"addLaneRouterHint": "Native là gọi thẳng API của hãng. Chọn profile AI Router thì cấu hình được ghi vào config riêng của lane này lúc tạo.",
"addLaneRouterHint": "Native là gọi thẳng API của hãng. Chọn profile AI Router thì cấu hình được ghi vào config riêng của workspace này lúc tạo.",
"addLaneRouterLabel": "Endpoint model",
"addLaneRouterLoading": "Đang tải danh sách router…",
"addLaneRouterProfileLabel": "Profile router",
@@ -57,7 +57,7 @@
"addLaneNoTools": "Chưa có AI tool nào được cài và bật.",
"addLaneNoToolsLink": "Mở Cài đặt → AI Tools",
"addLaneRouterNativeTool": "Native ({{tool}})",
"addLaneToolHint": "CLI nào điều khiển lane này. Không thể thay đổi sau này.",
"addLaneToolHint": "CLI nào điều khiển workspace này. Không thể thay đổi sau này.",
"addLaneWorktreeDetached": "(detached)",
"addLaneWorktreesMore": "+{{count}} nữa",
"addLaneWorktreesTitle": "Worktree của repo này ({{count}})",
@@ -101,7 +101,7 @@
"changes.openFile": "Mở {{path}}",
"changes.quickOpen": "Tìm file theo đường dẫn…",
"changes.range.branch": "So nhánh gốc",
"changes.range.branchHint": "base...HEAD — toàn bộ phần nhánh của lane này thêm vào so với nhánh gốc",
"changes.range.branchHint": "base...HEAD — toàn bộ phần nhánh của workspace này thêm vào so với nhánh gốc",
"changes.range.work": "Chưa commit",
"changes.range.workHint": "Working tree so với HEAD — những gì vừa sửa mà chưa commit",
"changes.refresh": "Tải lại",
@@ -117,20 +117,20 @@
"changes.stage": "Đưa file này vào index",
"changes.title": "Thay đổi",
"changes.truncated": "Một phần thay đổi đã bị lược bớt để bảng này còn mượt.",
"changes.unavailable": "Thư mục của lane này không phải repo git đọc được.",
"changes.unavailable": "Thư mục của workspace này không phải repo git đọc được.",
"changes.unstage": "Bỏ file này khỏi index",
"changes.wholeFile": "Toàn bộ file",
"confirmRemoveCancel": "Hủy",
"confirmRemoveConfirm": "Xóa",
"confirmRemoveMessage": "Hành động này không thể hoàn tác.",
"confirmRemoveTitle": "Xóa làn đường?",
"confirmRemoveTitle": "Xóa workspace?",
"countDead": "đã chết",
"countNeedsYou": "cần bạn",
"countRunning": "đang chạy",
"countTotal": "làn đường",
"countTotal": "workspace",
"destructive.blocked.adopted": "Đây là thư mục đã nhận và không thể thay đổi bằng thao tác worktree.",
"destructive.blocked.missing": "Thư mục làn đường không còn, nên không thể tiếp tục thao tác này.",
"destructive.blocked.unreadable": "Không thể đọc thư mục làn đường như một Git worktree, nên không thể tiếp tục thao tác này.",
"destructive.blocked.missing": "Thư mục workspace không còn, nên không thể tiếp tục thao tác này.",
"destructive.blocked.unreadable": "Không thể đọc thư mục workspace như một Git worktree, nên không thể tiếp tục thao tác này.",
"destructive.cancel": "Hủy",
"destructive.count.bytesEstimate": "Dung lượng giải phóng (ước tính)",
"destructive.count.dirty": "Tệp đã sửa",
@@ -142,26 +142,26 @@
"destructive.count.untracked": "Tệp chưa theo dõi",
"destructive.force": "Tôi hiểu các commit chưa đẩy này sẽ bị loại bỏ.",
"destructive.loading": "Đang tải trạng thái hiện tại…",
"destructive.notice.activeSessionSkipped": "Một hoặc nhiều phiên trong làn đường này vẫn đang hoạt động và sẽ được giữ lại, không bị dọn.",
"destructive.notice.adopted": "Lane này trỏ tới thư mục của bạn. Chỉ bản ghi trên bảng điều khiển bị xóa — thư mục và các tệp bên trong không bị thay đổi.",
"destructive.notice.missing": "Thư mục lane đã không còn. Việc xóa chỉ bỏ bản ghi trên bảng điều khiển; không tệp nào trên đĩa và không bản ghi Git nào bị đụng tới.",
"destructive.notice.unreadable": "Không thể đọc thư mục lane như một Git worktree. Điều đó không ảnh hưởng gì: chỉ bản ghi trên bảng điều khiển bị xóa, thư mục không bao giờ bị đụng tới.",
"destructive.notice.activeSessionSkipped": "Một hoặc nhiều phiên trong workspace này vẫn đang hoạt động và sẽ được giữ lại, không bị dọn.",
"destructive.notice.adopted": "Workspace này trỏ tới thư mục của bạn. Chỉ bản ghi trên bảng điều khiển bị xóa — thư mục và các tệp bên trong không bị thay đổi.",
"destructive.notice.missing": "Thư mục workspace đã không còn. Việc xóa chỉ bỏ bản ghi trên bảng điều khiển; không tệp nào trên đĩa và không bản ghi Git nào bị đụng tới.",
"destructive.notice.unreadable": "Không thể đọc thư mục workspace như một Git worktree. Điều đó không ảnh hưởng gì: chỉ bản ghi trên bảng điều khiển bị xóa, thư mục không bao giờ bị đụng tới.",
"destructive.purge.confirm": "Xóa lịch sử",
"destructive.purge.message": "Hãy xác nhận các số liệu hiện tại trước khi xóa lịch sử phiên đã lưu.",
"destructive.purge.title": "Xóa lịch sử làn đường?",
"destructive.remove.confirm": "Xóa bản ghi lane",
"destructive.remove.message": "Chỉ bản ghi của lane trên bảng điều khiển bị xóa. Working tree, nhánh và cơ sở dữ liệu của lane được giữ nguyên — bạn tự xóa nếu thực sự muốn bỏ chúng.",
"destructive.remove.title": "Xóa bản ghi lane?",
"destructive.purge.title": "Xóa lịch sử workspace?",
"destructive.remove.confirm": "Xóa bản ghi workspace",
"destructive.remove.message": "Chỉ bản ghi của workspace trên bảng điều khiển bị xóa. Working tree, nhánh và cơ sở dữ liệu của workspace được giữ nguyên — bạn tự xóa nếu thực sự muốn bỏ chúng.",
"destructive.remove.title": "Xóa bản ghi workspace?",
"destructive.reset.confirm": "Đặt lại worktree",
"destructive.reset.message": "Hãy xác nhận các số liệu hiện tại trước khi đặt lại worktree này.",
"destructive.reset.survives": "Các tệp Git bỏ qua như node_modules và .env được giữ lại; các tệp chưa theo dõi sẽ bị xóa.",
"destructive.reset.title": "Đặt lại worktree được quản lý?",
"destructive.warning.no-remote": "Chưa cấu hình Git remote, nên không có bản sao lưu ở xa. Bạn vẫn có thể tiếp tục.",
"empty.body": "Mỗi lane là một thư mục làm việc mà dashboard theo dõi — pipeline, session và terminal của nó. Bấm vào bất kỳ đâu trên ô này để thêm, hoặc từ terminal:",
"empty.title": "Chưa có lane nào",
"empty.body": "Mỗi workspace là một thư mục làm việc mà dashboard theo dõi — pipeline, session và terminal của nó. Bấm vào bất kỳ đâu trên ô này để thêm, hoặc từ terminal:",
"empty.title": "Chưa có workspace nào",
"features.archived": "đã lưu trữ",
"features.live": "Phiên bản trực tiếp",
"features.viewingArchived": "Xem tính năng đã lưu trữ \"{{slug}}\" — lane tiếp tục chạy; đây là ảnh chụp nhanh chỉ đọc.",
"features.viewingArchived": "Xem tính năng đã lưu trữ \"{{slug}}\" — workspace tiếp tục chạy; đây là ảnh chụp nhanh chỉ đọc.",
"git.filesHeading_one": "{{count}} tệp thay đổi",
"git.filesHeading_other": "{{count}} tệp thay đổi",
"git.filesMore_one": "… và {{count}} tệp nữa",
@@ -177,9 +177,9 @@
"gitBar.fetchHint": "git fetch --all --prune",
"gitBar.errNoRemote": "Không có remote để push hoặc pull. Hãy thêm remote, hoặc đặt upstream cho nhánh này, trong terminal.",
"gitBar.inSync": "đã đồng bộ",
"gitBar.newLane": "Tạo lane mới",
"gitBar.newLane": "Tạo workspace mới",
"gitBar.newLaneHeld": "Nhánh đã được checkout tại {{path}} — git chỉ cho một worktree trên mỗi nhánh",
"gitBar.newLaneHint": "Mở Add lane để tạo worktree anh em trên nhánh đang chọn",
"gitBar.newLaneHint": "Mở Thêm workspace để tạo worktree anh em trên nhánh đang chọn",
"gitBar.noCommits": "chưa có commit",
"gitBar.noUpstream": "chưa push",
"gitBar.publish": "Publish…",
@@ -202,25 +202,25 @@
"kind.adopted": "đã nhận",
"kind.managed": "được quản lý",
"laneDetail.hide": "Ẩn chi tiết",
"laneDetail.show": "Chi tiết lane",
"laneDetail.show": "Chi tiết workspace",
"locks.held_one": "Đang giữ {{count}} khóa",
"locks.held_other": "Đang giữ {{count}} khóa",
"mode.repo": "Repo",
"mode.repoHint": "Gắn thẳng thư mục này làm lane, nguyên trạng — không worktree, không nhánh mới.",
"mode.repoHint": "Gắn thẳng thư mục này làm workspace, nguyên trạng — không worktree, không nhánh mới.",
"mode.worktree": "Worktree",
"mode.worktreeHint": "Tạo worktree git mới + nhánh mới từ repo nguồn.",
"moreActions": "Thêm hành động",
"pipelinePicker.label": "Mẫu pipeline",
"pipelinePicker.stageMismatch": "Stage \"{{stage}}\" không khớp node nào trong \"{{pipeline}}\" — khai báo một trong: {{nodes}}",
"preflightError": "Không thể tải trạng thái làn đường hiện tại.",
"preflightErrorWithMessage": "Không thể tải trạng thái làn đường hiện tại: {{message}}",
"preflightError": "Không thể tải trạng thái workspace hiện tại.",
"preflightErrorWithMessage": "Không thể tải trạng thái workspace hiện tại: {{message}}",
"proof.ticketReport": "Báo cáo nhiệm vụ",
"renameLabel": "Đổi tên làn đường này",
"renameLabel": "Đổi tên workspace này",
"reorderHint": "Kéo để sắp xếp · Alt+←/→",
"router.applied": "Đã áp dụng.",
"router.native": "Native ({{tool}})",
"router.nativeOnly": "{{tool}} chạy bằng cấu hình riêng — hồ sơ AI Router chỉ dùng được với Claude Code và Codex.",
"router.appliedRestart": "Đã áp dụng, nhưng phiên đang chạy trong lane này vẫn dùng model cũ — khởi động lại phiên để có hiệu lực.",
"router.appliedRestart": "Đã áp dụng, nhưng phiên đang chạy trong workspace này vẫn dùng model cũ — khởi động lại phiên để có hiệu lực.",
"router.label": "Model",
"router.restart": "Khởi động lại",
"router.restarted": "Đã khởi động lại phiên với router mới.",
@@ -233,17 +233,17 @@
"runtime.slot": "slot {{slot}}",
"runtime.steppedAsideTitle": "Đã lùi khỏi {{expected}} vì cổng đó đang bận",
"runtime.stop": "■ dừng",
"runtime.toggleTitle": "Chạy hoặc dừng stack ứng dụng của lane này (khác với phiên Claude của nó)",
"splitView.emptyPane": "Chưa chọn lane cho ô này.",
"runtime.toggleTitle": "Chạy hoặc dừng stack ứng dụng của workspace này (khác với phiên Claude của nó)",
"splitView.emptyPane": "Chưa chọn workspace cho ô này.",
"splitView.paneCount": "{{count}} ô",
"splitView.paneLaneLabel": "Bộ chọn lane cho ô",
"splitView.pickLane": "Chọn lane",
"splitView.paneLaneLabel": "Bộ chọn workspace cho ô",
"splitView.pickLane": "Chọn workspace",
"status.failed": "thất bại",
"status.idle": "rảnh",
"status.provisioning": "đang khởi tạo",
"status.running": "đang chạy",
"statusDead": "ĐÃ CHẾT",
"title": "Làn đường",
"title": "Workspace",
"tooltipStart": "Tạo một lần chạy ở chế độ hội thoại mà không có lời nhắc ban đầu; được điều khiển từ CLI hoặc qua tin nhắn",
"worktrees.heading_one": "{{count}} worktree",
"worktrees.heading_other": "{{count}} worktree",
@@ -276,10 +276,10 @@
"gitBar.loginSubmit": "Đăng nhập và thử lại",
"gitBar.loginCancel": "Huỷ",
"gitBar.merge": "Merge vào nhánh hiện tại",
"gitBar.mergeHint": "Merge nhánh đang chọn vào nhánh lane này đang đứng",
"gitBar.mergeHint": "Merge nhánh đang chọn vào nhánh workspace này đang đứng",
"gitBar.mergeUpstream": "Merge {{upstream}}",
"gitBar.mergeUpstreamHint": "Nhánh đã lệch với upstream: merge vào (conflict được giải quyết trong Changes panel)",
"gitBar.mergeConfirm": "Merge {{branch}} vào {{current}}? File trong lane sẽ thay đổi ngay - nếu có agent đang làm việc trong lane này, hãy để nó xong trước.",
"gitBar.mergeConfirm": "Merge {{branch}} vào {{current}}? File trong workspace sẽ thay đổi ngay - nếu có agent đang làm việc trong workspace này, hãy để nó xong trước.",
"gitBar.mergeConflicts": "Merge dừng ở {{count}} file conflict - hãy giải quyết trong danh sách bên dưới.",
"gitBar.merging": "Đang merge: hãy hoàn tất hoặc huỷ merge trước",
"changes.conflictsGroup": "Conflict khi merge",
+4 -4
View File
@@ -33,12 +33,12 @@
},
"errors": {
"attachFailed": "Không gắn được vào run: {{message}}",
"attachedToLiveInstead": "Phiên trước của làn đường này vẫn đang chạy nên không thể bắt đầu phiên mới — đã gắn vào phiên đang chạy thay vào đó. Bấm Kill trong Active runs để bắt đầu lại từ đầu.",
"attachedToLiveInstead": "Phiên trước của workspace này vẫn đang chạy nên không thể bắt đầu phiên mới — đã gắn vào phiên đang chạy thay vào đó. Bấm Kill trong Active runs để bắt đầu lại từ đầu.",
"cwdRequired": "Cần thư mục làm việc để bắt đầu run",
"killFailed": "Không dừng được: {{message}}",
"laneCreateFailed": "Không thể tạo làn đường: {{message}}",
"noLaneSelected": "Chưa chọn làn đường hoặc không thể tạo làn đường",
"noRunIdReturned": "Không trả về ID run từ khởi động làn đường",
"laneCreateFailed": "Không thể tạo workspace: {{message}}",
"noLaneSelected": "Chưa chọn workspace hoặc không thể tạo workspace",
"noRunIdReturned": "Không trả về ID run từ khởi động workspace",
"runStartedButNotAttached": "Run đã khởi động nhưng không thể gắn vào bảng điều khiển: {{message}}",
"sendFailed": "Không gửi được: {{message}}",
"startFailed": "Không khởi động được: {{message}}"
+14 -14
View File
@@ -68,7 +68,7 @@
"title": "CLAUDE_HOME",
"description": "Thư mục gốc chứa dữ liệu phiên Claude Code. Thay đổi sẽ cập nhật ngay đường dẫn quét cho transcript và import.",
"placeholder": "/đường-dẫn/tới/claude-home",
"routersMissing": "Đã lưu. Các lane này dùng một hồ sơ AI Router mà thư mục mới không có, nên sẽ chạy Native cho đến khi bạn thêm hồ sơ đó ở đây hoặc chọn lại router cho lane: {{lanes}}"
"routersMissing": "Đã lưu. Các workspace này dùng một hồ sơ AI Router mà thư mục mới không có, nên sẽ chạy Native cho đến khi bạn thêm hồ sơ đó ở đây hoặc chọn lại router cho workspace: {{lanes}}"
},
"hooks": {
"title": "Cấu hình hook",
@@ -80,8 +80,8 @@
"failed": "Cài hooks thất bại"
},
"lanesSection": {
"title": "Lanes",
"description": "Dọn dẹp lane trên toàn máy.",
"title": "Workspace",
"description": "Dọn dẹp workspace trên toàn máy.",
"runHousekeeping": "Chạy dọn dẹp",
"dryRun": "Chạy thử (dry run)",
"gcResult": "Đã dọn {{reaped}} tiến trình, cắt {{capped}} log"
@@ -474,7 +474,7 @@
},
"aiRouter": {
"title": "AI Router",
"description": "Các endpoint đứng trước Claude Code và Codex — 9Router, OpenRouter, hoặc gateway tự dựng. Lưu ở đây rồi chọn lúc tạo lane.",
"description": "Các endpoint đứng trước Claude Code và Codex — 9Router, OpenRouter, hoặc gateway tự dựng. Lưu ở đây rồi chọn lúc tạo workspace.",
"add": "Thêm profile",
"edit": "Sửa",
"remove": "Xoá",
@@ -487,7 +487,7 @@
"keySet": "Đã lưu",
"keyMissing": "Chưa có",
"fieldLabel": "Tên",
"fieldLabelHint": "Tên bạn sẽ thấy trong form tạo lane. Gõ tự do.",
"fieldLabelHint": "Tên bạn sẽ thấy trong form tạo workspace. Gõ tự do.",
"fieldBaseUrl": "Base URL",
"fieldBaseUrlHint": "Endpoint tương thích OpenAI/Anthropic của router, ví dụ http://localhost:20128/v1",
"fieldApiKey": "API key",
@@ -506,9 +506,9 @@
},
"save": "Lưu",
"cancel": "Huỷ",
"removeConfirm": "Xoá profile \"{{label}}\"? Mọi lane đang dùng nó sẽ được đưa về API gốc của hãng ngay lập tức — cả trên đĩa lẫn trong dashboard.",
"removeConfirm": "Xoá profile \"{{label}}\"? Mọi workspace đang dùng nó sẽ được đưa về API gốc của hãng ngay lập tức — cả trên đĩa lẫn trong dashboard.",
"loading": "Đang tải danh sách profile…",
"lanesFailed": "Đã lưu, nhưng {{count}} lane không cập nhật được và vẫn đang giữ endpoint cũ: {{lanes}}",
"lanesFailed": "Đã lưu, nhưng {{count}} workspace không cập nhật được và vẫn đang giữ endpoint cũ: {{lanes}}",
"test": "Kiểm tra",
"testing": "Đang kiểm tra…",
"testOk": "Endpoint đã trả lời — {{model}} dùng được với key này.",
@@ -516,7 +516,7 @@
},
"aiTools": {
"title": "AI Tools",
"description": "Các CLI lập trình AI mà AgentHub chạy được trong lane. Nhận diện qua PATH của máy; tool bị tắt sẽ ẩn khỏi Add lane.",
"description": "Các CLI lập trình AI mà AgentHub chạy được trong workspace. Nhận diện qua PATH của máy; tool bị tắt sẽ ẩn khỏi Thêm workspace.",
"installed": "Đã cài ({{count}} tool)",
"available": "Có thể cài ({{count}})",
"refresh": "Làm mới",
@@ -585,14 +585,14 @@
},
"codexHome": {
"title": "CODEX_HOME",
"description": "Thư mục gốc chứa config và nhật ký phiên của Codex. Dashboard đọc từ đây, ghi layer AI Router vào đây, và mở mọi lane codex với thư mục này.",
"restartNote": "Lane codex đang chạy vẫn dùng thư mục cũ, và hiện là idle, cho tới khi được khởi động lại.",
"description": "Thư mục gốc chứa config và nhật ký phiên của Codex. Dashboard đọc từ đây, ghi layer AI Router vào đây, và mở mọi workspace codex với thư mục này.",
"restartNote": "Workspace codex đang chạy vẫn dùng thư mục cũ, và hiện là idle, cho tới khi được khởi động lại.",
"placeholder": "/đường-dẫn/tới/codex-home",
"routersFailed": "Đã lưu, nhưng không ghi lại được layer AI Router cho: {{lanes}}"
},
"codexHooks": {
"title": "Hook của Codex",
"description": "Cho Codex báo về AgentHub trong lúc làm việc: từng lần gọi công cụ trong dòng thời gian của phiên, \"Codex cần bạn cho phép\" trên thẻ lane và qua thông báo, và trạng thái chạy/nghỉ ngay khi một lượt bắt đầu hay kết thúc. Token và chi phí vẫn lấy từ nhật ký của Codex dù có hook hay không.",
"description": "Cho Codex báo về AgentHub trong lúc làm việc: từng lần gọi công cụ trong dòng thời gian của phiên, \"Codex cần bạn cho phép\" trên thẻ workspace và qua thông báo, và trạng thái chạy/nghỉ ngay khi một lượt bắt đầu hay kết thúc. Token và chi phí vẫn lấy từ nhật ký của Codex dù có hook hay không.",
"notInstalled": "Chưa cài",
"untrusted": "Đã cài — đang chờ bạn tin cậy trong Codex",
"trusted": "Đã cài và được tin cậy",
@@ -614,7 +614,7 @@
"notInstalled": "Chưa cài",
"installed": "Đã cài",
"disabled": "Đã cài nhưng đang tắt trong hooks.json",
"note": "agy nạp hook ở lần khởi động tiếp theo và không hỏi tin cậy. File này dùng chung, nên Antigravity IDE cũng chạy hook; AgentHub bỏ qua những gì IDE gửi. agy không có hook cho lời hỏi quyền, nên lane Antigravity không thể hiện \"cần bạn cấp quyền\".",
"note": "agy nạp hook ở lần khởi động tiếp theo và không hỏi tin cậy. File này dùng chung, nên Antigravity IDE cũng chạy hook; AgentHub bỏ qua những gì IDE gửi. agy không có hook cho lời hỏi quyền, nên workspace Antigravity không thể hiện \"cần bạn cấp quyền\".",
"install": "Cài đặt",
"reinstall": "Cài lại",
"refresh": "Làm mới",
@@ -629,9 +629,9 @@
"update": "Cập nhật…",
"usage_one": "{{count}} phiên · ${{cost}}",
"usage_other": "{{count}} phiên · ${{cost}}",
"lanes": "Lane:",
"lanes": "Workspace:",
"defaultModel": "Model mặc định",
"defaultModelHint": "Model mà một lần chạy mới trong lane của công cụ này dùng. Ô chọn khi chạy vẫn đổi được.",
"defaultModelHint": "Model mà một lần chạy mới trong workspace của công cụ này dùng. Ô chọn khi chạy vẫn đổi được.",
"saveDefaultModel": "Lưu model mặc định",
"config": "File cấu hình",
"configHint": "Các file cấu hình của chính công cụ này. Khi lưu, cú pháp được kiểm tra, bản cũ được sao lưu vào thư mục dữ liệu của AgentHub, và sẽ không lưu nếu file đã đổi từ lúc mở.",
+5 -5
View File
@@ -64,7 +64,7 @@
"why": "Khi biểu đồ tổng hợp đánh dấu một phiên đáng ngờ, đây là nơi bạn đọc chính xác chuyện gì đã xảy ra, theo đúng thứ tự."
},
"lanes": {
"what": "Toàn bộ bản ghi giai đoạn của mọi lane và feature đã lưu trữ, đếm theo từng node pipeline: số lần vào giai đoạn, số lần pass, và bao nhiêu lần pass có kèm bằng chứng.",
"what": "Toàn bộ bản ghi giai đoạn của mọi workspace và feature đã lưu trữ, đếm theo từng node pipeline: số lần vào giai đoạn, số lần pass, và bao nhiêu lần pass có kèm bằng chứng.",
"howToRead": "Mỗi node một dòng. Thanh bar so tỉ lệ với node bận nhất, chia ba màu: xanh lá (pass có bằng chứng), vàng (pass nhưng chỉ khai báo), đỏ (thất bại). Số bên phải là pass / số lần vào và tỉ lệ pass.",
"why": "Node có dải vàng dài là giai đoạn ai cũng khai báo nhưng không ai chứng minh; node chưa bao giờ tới là giai đoạn pipeline của bạn thực tế không dùng."
},
@@ -236,8 +236,8 @@
"title": "Tỉ lệ pass các node pipeline",
"subtitle": "Mỗi giai đoạn trong pipeline của bạn thực sự pass bao nhiêu lần",
"pipelineLabel": "Mẫu pipeline",
"laneLabel": "Lane",
"allLanes": "Tất cả lane",
"laneLabel": "Workspace",
"allLanes": "Tất cả workspace",
"runs_one": "{{count}} lượt chạy được ghi nhận",
"runs_other": "{{count}} lượt chạy được ghi nhận",
"legendEvidence": "pass kèm bằng chứng",
@@ -245,8 +245,8 @@
"legendFailed": "thất bại",
"never": "chưa từng tới",
"copyCommand": "Sao chép: {{command}}",
"empty": "Chưa lane nào ghi nhận giai đoạn. Tạo lane bằng",
"emptyLane": "Lane này chưa ghi nhận giai đoạn nào.",
"empty": "Chưa workspace nào ghi nhận giai đoạn. Tạo workspace bằng",
"emptyLane": "Workspace này chưa ghi nhận giai đoạn nào.",
"legendDetected": "đã chạm, chưa khai báo",
"detectedOnly": "suy đoán ×{{count}}"
},
@@ -499,7 +499,9 @@ describe("Workspace layout", () => {
expect(strip.firstElementChild).toBe(screen.getByTestId("lane-tile-add"));
await user.click(screen.getByTestId("lane-tile-add"));
expect(await screen.findByText("Create a lane from a working directory")).toBeInTheDocument();
expect(
await screen.findByText("Create a workspace from a working directory")
).toBeInTheDocument();
});
it("gives every lane a tile in the carousel", async () => {
@@ -5428,12 +5428,12 @@ exports[`screen snapshots > Run 1`] = `
<span
class="block text-sm font-medium text-fg-primary"
>
No lanes yet
No workspaces yet
</span>
<p
class="mt-0.5 text-xs text-fg-muted"
>
A lane is one working directory the dashboard follows — its pipeline, sessions and terminal. Click anywhere on this card to add one, or from a terminal:
A workspace is one working directory the dashboard follows — its pipeline, sessions and terminal. Click anywhere on this card to add one, or from a terminal:
<code
class="rounded bg-surface-2 px-1.5 py-0.5 text-[11px] text-fg-secondary"
@@ -5464,7 +5464,7 @@ exports[`screen snapshots > Run 1`] = `
d="M12 5v14"
/>
</svg>
Add lane
Add workspace
</span>
</button>
<div
@@ -7324,7 +7324,7 @@ exports[`screen snapshots > Settings 1`] = `
d="M18 9a9 9 0 0 1-9 9"
/>
</svg>
Lanes
Workspaces
</button>
<button
class="inline-flex items-center gap-1.5 text-xs whitespace-nowrap px-2.5 py-1.5 rounded-lg border transition-colors flex-shrink-0 border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
@@ -8339,12 +8339,12 @@ exports[`screen snapshots > Settings 1`] = `
d="M18 9a9 9 0 0 1-9 9"
/>
</svg>
Lanes
Workspaces
</h3>
<p
class="text-xs text-fg-muted mb-4"
>
Machine-wide lane housekeeping.
Machine-wide workspace housekeeping.
</p>
<div
class="card p-5 space-y-4"
@@ -11029,7 +11029,7 @@ exports[`screen snapshots > Workflows 1`] = `
<h2
class="text-sm font-semibold text-fg-primary"
>
Lane Pipeline Pass Rates
Workspace Pipeline Pass Rates
</h2>
<button
aria-expanded="false"
@@ -11079,7 +11079,7 @@ exports[`screen snapshots > Workflows 1`] = `
<p
class="text-sm text-fg-muted"
>
No lane has recorded a stage yet. Create one with
No workspace has recorded a stage yet. Create one with
<code>
agenthub lanes add --cwd $(pwd)