mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
fix(status-cards): give agents the person's restore route when archiving a card
Archive and restore responses carried no board route and the PATCH operation had no description, so a chat agent that archived a card could only tell the person to 'clear archived' via the API. Single-card responses (create, get, patch) now include href, the company-scoped Status page route, and the PATCH description explains that archive keeps the card and history, that a person restores it from the Status page, and that restoring starts a fresh summary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ed1de93f80
commit
2010e43746
5 files changed
+29
-6
No files matched your search
@@ -102,6 +102,8 @@ export const statusCardSchema = z.object({
|
||||
watchedIssueCount: z.number().int().nonnegative().optional(),
|
||||
todayTokens: z.number().int().nonnegative().optional(),
|
||||
todayCostCents: z.number().nonnegative().optional(),
|
||||
/** Company-scoped board route where people view, restore and refresh the card. */
|
||||
href: z.string().optional(),
|
||||
});
|
||||
|
||||
export const statusCardUpdateChangeSchema = z.object({
|
||||
|
||||
@@ -576,6 +576,14 @@ describe("openapi routes", () => {
|
||||
expect(dismiss.description).toContain("viewers may dismiss their own");
|
||||
});
|
||||
|
||||
it("tells agents that archiving keeps a status card and where a person restores it", async () => {
|
||||
const res = await request(createApp()).get("/api/openapi.json");
|
||||
const patch = res.body.paths["/api/status-cards/{id}"].patch;
|
||||
expect(patch.description).toContain("it is not deleted");
|
||||
expect(patch.description).toContain("give them the returned href as a Markdown link");
|
||||
expect(patch.description).toContain("Restoring (archived: false) starts a fresh full summary");
|
||||
});
|
||||
|
||||
it("documents exact failed-run selection and durable accepted retry responses", async () => {
|
||||
const res = await request(createApp()).get("/api/openapi.json");
|
||||
const wake = res.body.paths["/api/agents/{id}/wakeup"].post;
|
||||
|
||||
@@ -211,7 +211,9 @@ describeEmbeddedPostgres("status card routes", () => {
|
||||
|
||||
const archived = await request(app).patch(`/api/status-cards/${created.body.id}`).send({ archived: true });
|
||||
expect(archived.status).toBe(200);
|
||||
expect(archived.body).toMatchObject({ archivedAt: expect.any(String), generatingIssueId: null });
|
||||
// The person's restore route travels with the archive receipt, so an agent can link it.
|
||||
expect(archived.body).toMatchObject({ archivedAt: expect.any(String), generatingIssueId: null, href: `/${company.issuePrefix}/status/${created.body.id}` });
|
||||
expect((await request(app).get(`/api/status-cards/${created.body.id}`)).body.href).toBe(`/${company.issuePrefix}/status/${created.body.id}`);
|
||||
expect(await db.select().from(issues).where(eq(issues.id, created.body.generatingIssueId)).then((rows) => rows[0]?.status)).toBe("cancelled");
|
||||
expect((await request(app).get(`/api/companies/${company.id}/status-cards`)).body).toEqual([]);
|
||||
expect((await request(app).get(`/api/companies/${company.id}/status-cards?archived=true`)).body).toHaveLength(1);
|
||||
|
||||
@@ -3461,6 +3461,9 @@ registerCurrentRoute({
|
||||
path: "/api/status-cards/{id}",
|
||||
tags: ["status-cards"],
|
||||
summary: "Update, archive, or restore a status card",
|
||||
description: "Archiving (archived: true) hides the card from the active list but keeps the card, its summary and its update history; it is not deleted. "
|
||||
+ "A person views and restores archived cards on the company's Status page (Show archived, then Restore); give them the returned href as a Markdown link. "
|
||||
+ "Restoring (archived: false) starts a fresh full summary for a card with a compiled query, so restored content is regenerated before it is current.",
|
||||
body: patchStatusCardSchema,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Router, type Request } from "express";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { companies, type Db } from "@paperclipai/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
createStatusCardSchema,
|
||||
listStatusCardsQuerySchema,
|
||||
@@ -30,6 +31,13 @@ export function statusCardRoutes(db: Db, opts: { heartbeat?: IssueAssignmentWake
|
||||
const issueSvc = issueService(db);
|
||||
const heartbeat = opts.heartbeat ?? heartbeatService(db);
|
||||
|
||||
// People view, restore and refresh a card on the company's Status page. Single-card responses carry that
|
||||
// route so an agent can give the person a usable link instead of an API instruction.
|
||||
async function withHref<T extends { id: string; companyId: string }>(card: T) {
|
||||
const [company] = await db.select({ issuePrefix: companies.issuePrefix }).from(companies).where(eq(companies.id, card.companyId)).limit(1);
|
||||
return company ? { ...card, href: `/${company.issuePrefix}/status/${card.id}` } : card;
|
||||
}
|
||||
|
||||
async function assertStatusCardsEnabled() {
|
||||
const experimental = await settings.getExperimental();
|
||||
if (experimental.enableStatusCards !== true) throw notFound("Status cards are not enabled");
|
||||
@@ -168,7 +176,7 @@ export function statusCardRoutes(db: Db, opts: { heartbeat?: IssueAssignmentWake
|
||||
try {
|
||||
const compile = await enqueueCompile(req, card.id);
|
||||
await logMutation(req, companyId, "status_card.created", card.id, { state: card.state });
|
||||
res.status(201).json(publicCard(compile.card));
|
||||
res.status(201).json(await withHref(publicCard(compile.card)));
|
||||
} catch (error) {
|
||||
await service.remove(card.id);
|
||||
throw error;
|
||||
@@ -179,9 +187,9 @@ export function statusCardRoutes(db: Db, opts: { heartbeat?: IssueAssignmentWake
|
||||
await assertStatusCardsEnabled();
|
||||
const card = await getAccessibleResource(req, res, service.getById(req.params.id as string), "Status card not found");
|
||||
if (!card) return;
|
||||
res.json(publicCard(await service.hydrate(card, {
|
||||
res.json(await withHref(publicCard(await service.hydrate(card, {
|
||||
issueReadCondition: await issueReadSqlCondition(db, req.actor),
|
||||
})));
|
||||
}))));
|
||||
});
|
||||
|
||||
router.patch("/status-cards/:id", validate(patchStatusCardSchema), async (req, res) => {
|
||||
@@ -203,7 +211,7 @@ export function statusCardRoutes(db: Db, opts: { heartbeat?: IssueAssignmentWake
|
||||
fields: Object.keys(req.body),
|
||||
archived: Boolean(updated.archivedAt),
|
||||
});
|
||||
res.json(publicCard(compile?.card ?? restore?.card ?? updated));
|
||||
res.json(await withHref(publicCard(compile?.card ?? restore?.card ?? updated)));
|
||||
});
|
||||
|
||||
router.delete("/status-cards/:id", async (req, res) => {
|
||||
|
||||
Reference in new issue
Block a user