feat(projects): start a task at a workspace root and resolve the child repo from the first message - #17
Conversation
A project that is not a git work tree but holds git repos as immediate children (~/Projects) is a workspace root. Starting a task there used to run the agent at the root; now the composer resolves which child repo the message is about and the session is created under that repo. Server: GET /api/projects/:id/resolve-target?text= ranks every child repo by name (exact token 100, whole mention 80, prefix 40, then recency; the package.json name counts as a name) and POST /api/projects/:id/descend registers the chosen child as an explicit project, refusing anything that is not an immediate child with .git. No schema change; the projects list payload is untouched so legacy rows cost nothing. Client: a chip above the composer shows the resolved target once the top score reaches 80, with a searchable picker to override or keep the root. Send descends first, then creates the session under the child. The session_upserted event now carries the project's origin so a repo the indexer had only discovered shows up in the sidebar the moment its first session lands, in every viewer, without a refetch.
…y repo Selecting a project loads its file tree for @-mentions ten levels deep, stat'ing every file. On a workspace root that means every file of every repo underneath: ~/Projects with a hundred checkouts pinned the event loop for minutes and starved every other request. A workspace root now lists its children only; the walk is unchanged for real repos.
|
|
- list a workspace root's own entries only (depth 0) in the file tree - enumerate child repos once per request via a shared snapshot - reject symlinked/oversized package.json manifests when scoring - propagate readdir/stat failures instead of reporting a non-workspace - whole-mention score short names the ASCII tokenizer mangles (CJK, c++)
A send inside the debounce window previously created the session at the workspace root; allocation now awaits resolve-target with the submitted text, honors an explicit pin (including keep-at-root), and surfaces resolver failures instead of falling back to the root. Preview requests are project-scoped and text-capped, pins reset on project change, and cached project origin updates are monotonic so a stale index event can never demote an explicit project.
Review (maintainer)Solid shape — server-authoritative child validation, no model call, clean client integration. Review found five defects worth fixing before merge, so I pushed two commits on top (branch push was enabled):
Verification: workspace-target server tests 14/14, client dom tests 25/25, locale parity green, both tsconfigs typecheck clean, eslint clean on all touched files. Deferred (non-blocking): detecting git work-tree membership for the root via LGTM, merging. |
Closes #16.
What this changes
A project that is not a git work tree but holds git repos as immediate children (
~/Projects) is treated as a workspace root. Starting a task there now resolves which child repo the first message is about and creates the session under that repo.GET /api/projects/:id/resolve-target?text=ranks every child repo by name — exact token 100, whole mention 80, prefix 40, then recency; thepackage.jsonname counts as a name. No model call, onereaddirplus a stat per child.POST /api/projects/:id/descend { path }registers the chosen child as an explicit project (or promotes a row the indexer had only discovered) and refuses anything that is not an immediate child with.git. It is the only write.→ gajae-code-app), with a searchable picker to override or keep the root. Send descends first, then creates the session under the child.session_upsertednow carries the project'sorigin, so a repo that just became explicit shows up in the sidebar the moment its first session lands, in every viewer, without a refetch.GET /api/projects/:id/fileslists the children only. The ten-level walk stat'ed every file of every repo underneath and pinned the event loop for ~37 s on a root with a hundred checkouts, starving every other request. The walk is unchanged for real repos.No schema change; the projects list payload is untouched, so legacy rows cost nothing. Nothing changes for a project that is already a repo.
Why
Registering every repository as its own project is the main friction for anyone who keeps many repos under one root. The runtime already thinks in "start at the root, descend to the repo the message is about"; the app did not. See #16 for the shape discussion — happy to rename the endpoints, change the scoring, or move the resolution into the composer entirely if you would rather keep the server thin.
Verification
npm run typecheck— clean.npm run lint— clean.npm test— server 600 pass / 0 fail (node:test), client 454 / 0, scripts 16 / 0, every Bun suite 0 fail. New:server/modules/projects/tests/workspace-target.test.ts(12: scoring rules incl. the whole-mention boundary and a Korean directory name, fixture workspace with hidden /node_modules/ non-git children, descend 201→200, un-archive, promotion of anautorow, rejection of traversal / non-child / non-git),src/components/chat/hooks/useWorkspaceTarget.dom.bun.test.tsx,src/components/chat/view/WorkspaceTargetChip.dom.bun.test.tsx, and an origin-promotion case inuseProjectsState.query.dom.bun.test.tsx.npm run check:identity— passed;npm run build:client— built.scripts/check-locale-parity.test.mjs— the newworkspaceTarget.*keys are in all ten locales.~/Projectswith 99 child repos through the dev server and a headless Chromium: typinghf-studio 빌드 왜 깨지는지 봐줘resolved the chip in ~320 ms, Send created the session withsessions.project_path = …/hf-studio, the runtime titled it, the sidebar showedhf-studiowith the session without a reload; picker filter, "Keep at root", and the upward/downward flip of the popup checked;descendrejected../, a grandchild, and a child without.git.npm run verifypasses, or I have said below which gate fails and why. — typecheck, lint, test, check:identity and build:client were run individually and pass; the fullverifychain (audit, licences, notices, Rust core) is running now and I will post the result as a comment.