mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-29 06:28:23 +02:00
* feat(projects): typed project resources + agent runtime injection
Adds a `project_resource` table that lets a project carry typed pointers
(github_repo today, more later) and surfaces them at agent runtime.
Server
- migration 065: project_resource (resource_type TEXT + resource_ref JSONB)
- sqlc CRUD + handler at /api/projects/{id}/resources
- claim handler attaches project_id/title + resources to issue tasks
Daemon
- TaskContextForEnv carries project context
- writes .multica/project/resources.json into workdir
- adds "## Project Context" block to CLAUDE.md / AGENTS.md / GEMINI.md
via type-dispatched formatter so new resource types just add a case
CLI
- multica project create --repo <url> attaches repos in one step
- multica project resource add/list/remove
Frontend
- Project create modal: Repos pill (workspace repos + ad-hoc URL)
- Project detail sidebar: collapsible Resources section with attach/remove
Docs
- New "Project Resources" chapter explaining the abstraction and
exactly what code to touch when adding a new resource type
Co-authored-by: multica-agent <github@multica.ai>
* fix(projects): transactional resources[] on create + generic CLI ref + test fix
Addresses review feedback on PR #1926:
1. CI red: TestProjectResourceLifecycle delete step called withURLParam
twice, which replaced the chi route context and dropped the project id.
Switched to the existing withURLParams helper from daemon_test.go.
2. POST /api/projects now accepts resources[] and attaches them in the
same transaction as the project. Invalid refs roll back the whole
create — no more half-attached projects on failure. Web modal + CLI
`project create --repo` both use the new bundled payload.
3. CLI `project resource add` now accepts a generic --ref '<json>' flag
so a new resource_type works without a CLI change. Per-type
shortcuts (--url for github_repo) remain as a convenience but are no
longer the only way in. Docs updated to drop the CLI from the
"files you must touch" list.
Adds two new server handler tests:
- TestCreateProjectAttachesResources (resources[] happy path)
- TestCreateProjectRollsBackOnInvalidResource (transactional rollback)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
88 lines
2.6 KiB
TypeScript
88 lines
2.6 KiB
TypeScript
import { queryOptions, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { api } from "../api";
|
|
import { projectKeys } from "./queries";
|
|
import type {
|
|
CreateProjectResourceRequest,
|
|
ListProjectResourcesResponse,
|
|
ProjectResource,
|
|
} from "../types";
|
|
|
|
export const projectResourceKeys = {
|
|
list: (wsId: string, projectId: string) =>
|
|
[...projectKeys.detail(wsId, projectId), "resources"] as const,
|
|
};
|
|
|
|
export function projectResourcesOptions(wsId: string, projectId: string) {
|
|
return queryOptions({
|
|
queryKey: projectResourceKeys.list(wsId, projectId),
|
|
queryFn: () => api.listProjectResources(projectId),
|
|
select: (data) => data.resources,
|
|
});
|
|
}
|
|
|
|
export function useCreateProjectResource(wsId: string, projectId: string) {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data: CreateProjectResourceRequest) =>
|
|
api.createProjectResource(projectId, data),
|
|
onSuccess: (created) => {
|
|
qc.setQueryData<ListProjectResourcesResponse>(
|
|
projectResourceKeys.list(wsId, projectId),
|
|
(old) =>
|
|
old && !old.resources.some((r) => r.id === created.id)
|
|
? {
|
|
...old,
|
|
resources: [...old.resources, created],
|
|
total: old.total + 1,
|
|
}
|
|
: old,
|
|
);
|
|
},
|
|
onSettled: () => {
|
|
qc.invalidateQueries({
|
|
queryKey: projectResourceKeys.list(wsId, projectId),
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteProjectResource(wsId: string, projectId: string) {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (resourceId: string) =>
|
|
api.deleteProjectResource(projectId, resourceId),
|
|
onMutate: async (resourceId) => {
|
|
await qc.cancelQueries({
|
|
queryKey: projectResourceKeys.list(wsId, projectId),
|
|
});
|
|
const prev = qc.getQueryData<ListProjectResourcesResponse>(
|
|
projectResourceKeys.list(wsId, projectId),
|
|
);
|
|
qc.setQueryData<ListProjectResourcesResponse>(
|
|
projectResourceKeys.list(wsId, projectId),
|
|
(old) =>
|
|
old
|
|
? {
|
|
...old,
|
|
resources: old.resources.filter(
|
|
(r: ProjectResource) => r.id !== resourceId,
|
|
),
|
|
total: old.total - 1,
|
|
}
|
|
: old,
|
|
);
|
|
return { prev };
|
|
},
|
|
onError: (_err, _id, ctx) => {
|
|
if (ctx?.prev) {
|
|
qc.setQueryData(projectResourceKeys.list(wsId, projectId), ctx.prev);
|
|
}
|
|
},
|
|
onSettled: () => {
|
|
qc.invalidateQueries({
|
|
queryKey: projectResourceKeys.list(wsId, projectId),
|
|
});
|
|
},
|
|
});
|
|
}
|