feat(kanban): collect project directory when creating boards (#63249)

This commit is contained in:
Teknium 2026-07-12 17:51:30 -07:00 committed by GitHub
parent 2bffeece66
commit aaf5691261
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 92 additions and 0 deletions

View file

@ -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" },

View file

@ -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()}

View file

@ -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 = (