6c605849d8
- /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.
87 lines
3.0 KiB
TypeScript
87 lines
3.0 KiB
TypeScript
/**
|
|
* @file PathPickButton.tsx
|
|
* @description The one "Browse" button beside every path field: it asks the
|
|
* server to open the operating system's own folder/file picker
|
|
* (`POST /api/lanes/pick-path`) and fills the field with the absolute path the
|
|
* user chose. A browser's picker cannot give a page that path, so the dialog
|
|
* opens on the server's desktop — which is only this user's screen when the
|
|
* browser runs on the same machine. Anywhere else (a LAN browser, a server
|
|
* with no desktop or no zenity/kdialog) a folder falls back to the in-app
|
|
* `FolderBrowseModal` (as it does on any other failure), and a file to typing
|
|
* the path.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { FolderOpen } from "lucide-react";
|
|
import { api } from "../lib/api";
|
|
import { toast } from "../lib/toast";
|
|
import { FolderBrowseModal } from "./lanes/FolderBrowseModal";
|
|
|
|
/** Server answers that mean "no native picker for this browser" — not a fault. */
|
|
const FALLBACK = new Set(["EREMOTE", "EBADORIGIN", "ENODIALOG"]);
|
|
|
|
export function PathPickButton({
|
|
kind = "dir",
|
|
start,
|
|
onPick,
|
|
showLabel = false,
|
|
}: {
|
|
kind?: "dir" | "file";
|
|
/** The field's current value — the picker opens there. */
|
|
start?: string;
|
|
onPick: (path: string) => void;
|
|
showLabel?: boolean;
|
|
}) {
|
|
const { t } = useTranslation(["lanes"]);
|
|
const [busy, setBusy] = useState(false);
|
|
const [fallback, setFallback] = useState(false);
|
|
const title = kind === "dir" ? t("browse.title") : t("browse.fileTitle");
|
|
|
|
const pick = async () => {
|
|
setBusy(true);
|
|
try {
|
|
const r = await api.lanes.pickPath(kind, start?.trim() || undefined);
|
|
if (r.path) onPick(r.path);
|
|
} catch (err) {
|
|
const code = (err as { code?: string }).code;
|
|
// A folder always has the in-app browser to fall back on, whatever went
|
|
// wrong with the native one (a crashed dialog, one already open).
|
|
if (kind === "dir") setFallback(true);
|
|
else if (code && FALLBACK.has(code)) toast.info(t("browse.typePath"));
|
|
else {
|
|
toast.error(err instanceof Error ? err.message : String(err));
|
|
}
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
type="button"
|
|
onClick={pick}
|
|
disabled={busy}
|
|
title={title}
|
|
aria-label={title}
|
|
className="flex shrink-0 items-center gap-1 self-stretch rounded-md border border-border-light px-2 text-xs text-fg-secondary hover:bg-surface-2 disabled:opacity-50"
|
|
>
|
|
<FolderOpen className="h-3.5 w-3.5" />
|
|
{showLabel && t("browse.button")}
|
|
</button>
|
|
{/* Its clicks stay here: a backdrop click must close only this modal,
|
|
not bubble on to a dialog this button sits in. */}
|
|
<div className="contents" onClick={(e) => e.stopPropagation()}>
|
|
<FolderBrowseModal
|
|
open={fallback}
|
|
initialPath={start?.trim() || undefined}
|
|
onSelect={onPick}
|
|
onClose={() => setFallback(false)}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|