mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
feat(kanban): collect project directory when creating boards (#63249)
This commit is contained in:
parent
2bffeece66
commit
aaf5691261
3 changed files with 92 additions and 0 deletions
23
plugins/kanban/dashboard/dist/index.js
vendored
23
plugins/kanban/dashboard/dist/index.js
vendored
|
|
@ -1888,6 +1888,7 @@
|
|||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [icon, setIcon] = useState("");
|
||||
const [projectDirectory, setProjectDirectory] = useState("");
|
||||
const [switchTo, setSwitchTo] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [err, setErr] = useState(null);
|
||||
|
|
@ -1912,6 +1913,7 @@
|
|||
name: name.trim() || autoName || undefined,
|
||||
description: description.trim() || undefined,
|
||||
icon: icon.trim() || undefined,
|
||||
default_workdir: projectDirectory.trim() || undefined,
|
||||
switch: switchTo,
|
||||
}).catch(function (e) {
|
||||
setErr(String(e && e.message ? e.message : e));
|
||||
|
|
@ -1967,6 +1969,27 @@
|
|||
className: "h-8",
|
||||
}),
|
||||
),
|
||||
h("div", { className: "flex flex-col gap-1" },
|
||||
h(Label, { className: "text-xs" },
|
||||
tx(t, "projectDirectory", "Project directory"), " ",
|
||||
h("span", { className: "text-muted-foreground" },
|
||||
tx(t, "projectDirectoryHint", "(recommended)"))),
|
||||
h(Input, {
|
||||
value: projectDirectory,
|
||||
onChange: function (e) { setProjectDirectory(e.target.value); },
|
||||
placeholder: tx(t, "projectDirectoryPlaceholder",
|
||||
"Absolute path to the project folder"),
|
||||
title: tx(t, "projectDirectoryHelp",
|
||||
"Git projects use preserved worktrees. Other folders use the directory directly. Leave blank only for temporary work."),
|
||||
className: "h-8",
|
||||
autoCapitalize: "none",
|
||||
autoCorrect: "off",
|
||||
spellCheck: false,
|
||||
}),
|
||||
h("div", { className: "text-xs text-muted-foreground" },
|
||||
tx(t, "projectDirectoryExplanation",
|
||||
"Sets the default location for task files so project output is preserved.")),
|
||||
),
|
||||
h("div", { className: "flex flex-col gap-1" },
|
||||
h(Label, { className: "text-xs" }, tx(t, "icon", "Icon"), " ",
|
||||
h("span", { className: "text-muted-foreground" },
|
||||
|
|
|
|||
|
|
@ -1982,6 +1982,7 @@ class CreateBoardBody(BaseModel):
|
|||
description: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
default_workdir: Optional[str] = None
|
||||
switch: bool = False
|
||||
|
||||
|
||||
|
|
@ -2037,6 +2038,20 @@ def list_boards(include_archived: bool = Query(False)):
|
|||
@router.post("/boards")
|
||||
def create_board_endpoint(payload: CreateBoardBody):
|
||||
"""Create a new board. Idempotent — ``slug`` collision returns existing."""
|
||||
default_workdir = None
|
||||
if payload.default_workdir:
|
||||
requested = Path(payload.default_workdir).expanduser()
|
||||
if not requested.is_absolute():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Project directory must be an absolute path.",
|
||||
)
|
||||
if not requested.is_dir():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Project directory must be an existing directory.",
|
||||
)
|
||||
default_workdir = str(requested.resolve())
|
||||
try:
|
||||
meta = kanban_db.create_board(
|
||||
payload.slug,
|
||||
|
|
@ -2044,6 +2059,7 @@ def create_board_endpoint(payload: CreateBoardBody):
|
|||
description=payload.description,
|
||||
icon=payload.icon,
|
||||
color=payload.color,
|
||||
default_workdir=default_workdir,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
|
@ -2052,6 +2068,7 @@ def create_board_endpoint(payload: CreateBoardBody):
|
|||
kanban_db.set_current_board(meta["slug"])
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
meta["default_workspace_kind"] = _default_workspace_kind(meta)
|
||||
return {"board": meta, "current": kanban_db.get_current_board()}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -138,6 +138,58 @@ def test_board_list_recommends_persistent_workspace_for_configured_workdir(
|
|||
assert boards["disposable"]["default_workspace_kind"] == "scratch"
|
||||
|
||||
|
||||
def test_create_board_persists_project_directory(client, tmp_path):
|
||||
"""The dashboard board form should anchor future tasks to its project."""
|
||||
project_dir = tmp_path / "project"
|
||||
project_dir.mkdir()
|
||||
|
||||
response = client.post(
|
||||
"/api/plugins/kanban/boards",
|
||||
json={
|
||||
"slug": "project-board",
|
||||
"name": "Project Board",
|
||||
"default_workdir": str(project_dir),
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
board = response.json()["board"]
|
||||
assert board["default_workdir"] == str(project_dir.resolve())
|
||||
assert board["default_workspace_kind"] == "dir"
|
||||
assert kb.read_board_metadata("project-board")["default_workdir"] == str(
|
||||
project_dir.resolve()
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", ["relative/project", "~/missing-project"])
|
||||
def test_create_board_rejects_invalid_project_directory(client, path):
|
||||
"""A board must not persist a path that cannot anchor worker output."""
|
||||
response = client.post(
|
||||
"/api/plugins/kanban/boards",
|
||||
json={"slug": "invalid-project", "default_workdir": path},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "project directory" in response.json()["detail"].lower()
|
||||
|
||||
|
||||
def test_new_board_dialog_collects_project_directory():
|
||||
"""Board creation should expose the setting that controls safe task defaults."""
|
||||
bundle = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "plugins"
|
||||
/ "kanban"
|
||||
/ "dashboard"
|
||||
/ "dist"
|
||||
/ "index.js"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
assert 'const [projectDirectory, setProjectDirectory] = useState("");' in bundle
|
||||
assert "Project directory" in bundle
|
||||
assert "Absolute path to the project folder" in bundle
|
||||
assert "default_workdir: projectDirectory.trim() || undefined" in bundle
|
||||
|
||||
|
||||
def test_dashboard_workspace_picker_explains_persistence_contract():
|
||||
"""Task creation must make scratch deletion visible without a hover."""
|
||||
bundle = (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue