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:
2026-10-06 11:31:26 +07:00
parent c067abc68f
commit 6c605849d8
104 changed files with 3478 additions and 1396 deletions
+39 -1
View File
@@ -1380,13 +1380,15 @@ export const api = {
*
* @returns {@link CostResult} — the aggregate cost breakdown across sessions.
*/
totalCost: (opts: { lane?: number } = {}) => {
totalCost: (opts: { lane?: number; date?: string } = {}) => {
// Scope the aggregate to the active data-scope, exactly like the sessions /
// stats / analytics endpoints — otherwise switching the Data scope selector
// left the Dashboard "total cost" showing the un-narrowed global total.
// `lane`: only the sessions that workspace owns.
const qs = new URLSearchParams({ tz_offset: String(new Date().getTimezoneOffset()) });
if (opts.lane !== undefined) qs.set("lane", String(opts.lane));
// `date` (local YYYY-MM-DD): only that day's usage, breakdown included.
if (opts.date) qs.set("date", opts.date);
applyScope(qs);
return request<CostResult>(`/pricing/cost?${qs.toString()}`);
},
@@ -2176,6 +2178,14 @@ export const api = {
run: {
/** GET /api/run - lanes with a live tmux-backed run, computed fresh from tmux state. */
list: () => request<RunListResponse>("/run"),
/** GET /api/run/history - persisted runs, newest first; `laneId` narrows to
* one workspace and `tool` (with it) to one tool's — `shell` = Terminal tabs. */
history: (opts: { laneId: number; tool?: string; limit?: number }) => {
const qs = new URLSearchParams({ laneId: String(opts.laneId) });
if (opts.tool) qs.set("tool", opts.tool);
if (opts.limit) qs.set("limit", String(opts.limit));
return request<{ items: RunHistoryItem[] }>(`/run/history?${qs.toString()}`);
},
/** GET /api/run/binary - whether the lane tool's CLI was found on PATH (fresh look). */
binary: (tool: LaneTool = "claude") =>
request<{ found: boolean; path: string | null }>(`/run/binary?tool=${tool}`),
@@ -2607,6 +2617,20 @@ export const api = {
parent: string | null;
entries: { name: string; path: string; isGitRepo: boolean }[];
}>(`/lanes/browse${path ? `?path=${encodeURIComponent(path)}` : ""}`),
/**
* POST /api/lanes/pick-path — open the OS's own folder/file picker on the
* server's desktop and wait for the user. Throws `.code` `EREMOTE` (not a
* browser on the server's machine), `EBADORIGIN` or `ENODIALOG` (no desktop
* / no picker program) when the caller should fall back to {@link browse}.
* @param kind `dir` or `file`.
* @param start Folder (or file) to open the picker at; home otherwise.
* @returns `{ path }`, or `{ cancelled: true }` when the user cancelled.
*/
pickPath: (kind: "dir" | "file", start?: string) =>
request<{ path?: string; cancelled?: boolean }>("/lanes/pick-path", {
method: "POST",
body: JSON.stringify({ kind, start }),
}),
/**
* GET /api/lanes/:id — fetch one lane.
* @param id The lane id.
@@ -2666,6 +2690,9 @@ export const api = {
*/
gitFile: (id: GitTarget, path: string, rev = "work") =>
request<LaneFileContent>(gitUrl(id, "/file", new URLSearchParams({ path, rev }))),
/** URL of GET /api/lanes/:id/git/raw — an image's bytes, for an `<img>`. */
gitRawUrl: (id: GitTarget, path: string, rev = "work") =>
`${BASE}${gitUrl(id, "/raw", new URLSearchParams({ path, rev }))}`,
/**
* PUT /api/lanes/:id/git/file — save the editor's text over the working
* file. `baseSha` is the `sha` it was opened at; a file that changed on
@@ -4011,6 +4038,17 @@ export interface RunListResponse {
items: RunHandle[];
}
/** One row of GET /api/run/history (`dashboard_runs`; only the fields read). */
export interface RunHistoryItem {
id: string;
tool: string;
status: string;
started_at: string;
ended_at: string | null;
/** The run's tmux pane is still up. */
isLive: boolean;
}
/** One suggested working directory for the Run page's cwd picker. */
export interface CwdSuggestion {
/** "dashboard" = this server's own cwd; "home" = user's home dir; "recent"