Allow external objects to define presentation metadata

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-23 00:35:05 +00:00
1 parent 85351a2c9a
commit 95acb60120
19 files changed
+228 -30

No files matched your search

@@ -182,12 +182,16 @@ describe("GitHub external object provider", () => {
providerKey: "github",
objectType: "pull_request",
externalId: "acme/app#pull/42",
displayKey: "GitHub PR",
iconKey: "github",
displayTitle: "Acme/App#42",
}),
expect.objectContaining({
providerKey: "github",
objectType: "issue",
externalId: "acme/app#issues/7",
displayKey: "GitHub Issue",
iconKey: "github",
displayTitle: "Acme/App#7",
}),
]);
@@ -198,22 +202,22 @@ describe("GitHub external object provider", () => {
[
"open",
{ state: "open", draft: false, merged: false, title: "Ship it", updated_at: "2026-04-24T01:02:03Z" },
{ statusKey: "open", statusLabel: "Open", statusCategory: "open", statusTone: "info", isTerminal: false },
{ statusKey: "open", statusLabel: "Open", statusIconKey: "git-pull-request", statusCategory: "open", statusTone: "info", isTerminal: false },
],
[
"draft",
{ state: "open", draft: true, merged: false, title: "WIP", updated_at: "2026-04-24T01:02:03Z" },
{ statusKey: "draft", statusLabel: "Draft", statusCategory: "waiting", statusTone: "warning", isTerminal: false },
{ statusKey: "draft", statusLabel: "Draft", statusIconKey: "clock", statusCategory: "waiting", statusTone: "warning", isTerminal: false },
],
[
"closed",
{ state: "closed", draft: false, merged: false, title: "Closed PR", updated_at: "2026-04-24T01:02:03Z" },
{ statusKey: "closed", statusLabel: "Closed", statusCategory: "closed", statusTone: "muted", isTerminal: true },
{ statusKey: "closed", statusLabel: "Closed", statusIconKey: "x-circle", statusCategory: "closed", statusTone: "muted", isTerminal: true },
],
[
"merged",
{ state: "closed", draft: false, merged: true, title: "Merged PR", updated_at: "2026-04-24T01:02:03Z" },
{ statusKey: "merged", statusLabel: "Merged", statusCategory: "succeeded", statusTone: "success", isTerminal: true },
{ statusKey: "merged", statusLabel: "Merged", statusIconKey: "git-merge", statusCategory: "succeeded", statusTone: "success", isTerminal: true },
],
])("resolves a %s pull request snapshot", async (_name, body, expected) => {
const fetch = vi.fn(async () => response(body));
@@ -235,6 +239,8 @@ describe("GitHub external object provider", () => {
ok: true,
snapshot: expect.objectContaining({
...expected,
displayKey: "GitHub PR",
iconKey: "github",
displayTitle: expect.stringContaining(String(body.title)),
remoteVersion: "2026-04-24T01:02:03Z",
etag: '"etag-1"',
@@ -253,12 +259,12 @@ describe("GitHub external object provider", () => {
[
"open",
{ state: "open", title: "Issue", state_reason: null, updated_at: "2026-04-24T01:02:03Z" },
{ statusKey: "open", statusLabel: "Open", statusCategory: "open", statusTone: "info", isTerminal: false },
{ statusKey: "open", statusLabel: "Open", statusIconKey: "circle-dot", statusCategory: "open", statusTone: "info", isTerminal: false },
],
[
"closed",
{ state: "closed", title: "Issue", state_reason: "completed", updated_at: "2026-04-24T01:02:03Z" },
{ statusKey: "closed_completed", statusLabel: "Closed: completed", statusCategory: "closed", statusTone: "muted", isTerminal: true },
{ statusKey: "closed_completed", statusLabel: "Closed: completed", statusIconKey: "circle", statusCategory: "closed", statusTone: "muted", isTerminal: true },
],
])("resolves a %s issue snapshot", async (_name, body, expected) => {
const fetch = vi.fn(async () => response(body));
@@ -275,6 +281,8 @@ describe("GitHub external object provider", () => {
ok: true,
snapshot: expect.objectContaining({
...expected,
displayKey: "GitHub Issue",
iconKey: "github",
data: expect.objectContaining({
provider: "github",
owner: "acme",
@@ -322,7 +330,7 @@ describe("GitHub external object provider", () => {
[
"not-found",
new Response("", { status: 404, headers: { etag: '"missing"' } }),
{ ok: true, snapshot: expect.objectContaining({ statusKey: "not_found", statusCategory: "archived", statusTone: "muted" }) },
{ ok: true, snapshot: expect.objectContaining({ displayKey: "GitHub PR", iconKey: "github", statusKey: "not_found", statusIconKey: "archive", statusCategory: "archived", statusTone: "muted" }) },
],
])("maps %s responses to provider-safe results", async (_name, githubResponse, expected) => {
const provider = createGitHubExternalObjectProvider({} as any, {
@@ -548,6 +556,8 @@ describeEmbeddedPostgres("externalObjectService", () => {
providerKey: "mocktracker",
objectType: "ticket",
externalId: "MOCK-123",
displayKey: "Mock Ticket",
iconKey: "circle-dot",
displayTitle: "Mock ticket 123",
confidence: "exact",
})),
@@ -557,9 +567,12 @@ describeEmbeddedPostgres("externalObjectService", () => {
return {
ok: true,
snapshot: {
displayKey: "Mock Ticket",
iconKey: "circle-dot",
displayTitle: `Resolved ${params.externalId}`,
statusKey: "ready",
statusLabel: "Ready",
statusIconKey: "check-circle",
statusCategory: "succeeded",
statusTone: "success",
ttlSeconds: 300,
@@ -579,6 +592,8 @@ describeEmbeddedPostgres("externalObjectService", () => {
providerKey: "mocktracker",
objectType: "ticket",
externalId: "MOCK-123",
displayKey: "Mock Ticket",
iconKey: "circle-dot",
pluginId: plugin!.id,
sanitizedCanonicalUrl: "https://mock.example/tickets/123",
});
@@ -586,6 +601,10 @@ describeEmbeddedPostgres("externalObjectService", () => {
const refreshed = await svc.refreshObject(object.id, { companyId, force: true });
expect(refreshed.object).toMatchObject({
displayKey: "Mock Ticket",
iconKey: "circle-dot",
displayTitle: "Resolved MOCK-123",
statusIconKey: "check-circle",
statusCategory: "succeeded",
statusTone: "success",
liveness: "fresh",
+21
View File
@@ -35,6 +35,8 @@ export interface ExternalObjectDetection {
providerKey: string;
objectType: string;
externalId: string;
displayKey?: string | null;
iconKey?: string | null;
displayTitle?: string | null;
confidence?: ExternalObjectMentionConfidence;
pluginId?: string | null;
@@ -50,9 +52,12 @@ export interface ExternalObjectDetector {
}
export interface ExternalObjectResolverSnapshot {
displayKey?: string | null;
iconKey?: string | null;
displayTitle?: string | null;
statusKey?: string | null;
statusLabel?: string | null;
statusIconKey?: string | null;
statusCategory: ExternalObjectStatusCategory;
statusTone: ExternalObjectStatusTone;
isTerminal?: boolean;
@@ -126,6 +131,7 @@ function objectChanged(before: ExternalObjectRecord, after: ExternalObjectRecord
return (
before.statusKey !== after.statusKey ||
before.statusLabel !== after.statusLabel ||
before.statusIconKey !== after.statusIconKey ||
before.statusCategory !== after.statusCategory ||
before.statusTone !== after.statusTone ||
before.isTerminal !== after.isTerminal
@@ -217,9 +223,12 @@ function objectSnapshot(object: ExternalObjectRecord): PluginExternalObjectRecor
externalId: object.externalId,
sanitizedCanonicalUrl: object.sanitizedCanonicalUrl,
canonicalIdentityHash: object.canonicalIdentityHash,
displayKey: object.displayKey,
iconKey: object.iconKey,
displayTitle: object.displayTitle,
statusKey: object.statusKey,
statusLabel: object.statusLabel,
statusIconKey: object.statusIconKey,
statusCategory: object.statusCategory,
statusTone: object.statusTone,
liveness: object.liveness,
@@ -285,6 +294,8 @@ function createPluginProviderDetector(
providerKey: detection.providerKey,
objectType: detection.objectType,
externalId: detection.externalId,
displayKey: detection.displayKey,
iconKey: detection.iconKey,
displayTitle: detection.displayTitle,
confidence: detection.confidence ?? "exact",
pluginId: provider.id,
@@ -399,6 +410,8 @@ export function externalObjectService(
externalId: detection.externalId,
sanitizedCanonicalUrl: canonical.sanitizedCanonicalUrl,
canonicalIdentityHash: canonical.canonicalIdentityHash,
displayKey: detection.displayKey ?? null,
iconKey: detection.iconKey ?? null,
displayTitle: detection.displayTitle ?? canonical.sanitizedDisplayUrl,
updatedAt: now,
};
@@ -415,6 +428,8 @@ export function externalObjectService(
set: {
sanitizedCanonicalUrl: values.sanitizedCanonicalUrl,
canonicalIdentityHash: values.canonicalIdentityHash,
displayKey: values.displayKey,
iconKey: values.iconKey,
displayTitle: values.displayTitle,
updatedAt: now,
},
@@ -663,7 +678,10 @@ export function externalObjectService(
id: object.id,
providerKey: object.providerKey,
objectType: object.objectType,
displayKey: object.displayKey,
iconKey: object.iconKey,
displayTitle: object.displayTitle,
statusIconKey: object.statusIconKey,
statusCategory: object.statusCategory,
statusTone: object.statusTone,
liveness: object.liveness,
@@ -796,9 +814,12 @@ export function externalObjectService(
const snapshot = result.snapshot;
const patch = {
displayKey: snapshot.displayKey ?? object.displayKey,
iconKey: snapshot.iconKey ?? object.iconKey,
displayTitle: snapshot.displayTitle ?? object.displayTitle,
statusKey: snapshot.statusKey ?? object.statusKey,
statusLabel: snapshot.statusLabel ?? object.statusLabel,
statusIconKey: snapshot.statusIconKey ?? object.statusIconKey,
statusCategory: snapshot.statusCategory,
statusTone: snapshot.statusTone,
isTerminal: snapshot.isTerminal ?? object.isTerminal,
@@ -105,6 +105,10 @@ function displayTitleFor(identity: GitHubObjectIdentity) {
return `${identity.owner}/${identity.repo}#${identity.number}`;
}
function displayKeyFor(identity: Pick<GitHubObjectIdentity, "objectType">) {
return identity.objectType === "pull_request" ? "GitHub PR" : "GitHub Issue";
}
function retryAfterSeconds(response: Response) {
const retryAfter = response.headers.get("retry-after");
if (retryAfter && /^[0-9]+$/.test(retryAfter)) return Number(retryAfter);
@@ -163,9 +167,12 @@ function failureFromGitHubResponse(response: Response): ExternalObjectResolveRes
function notFoundSnapshot(identity: GitHubObjectIdentity, etag: string | null): ExternalObjectResolverSnapshot {
return {
displayKey: displayKeyFor(identity),
iconKey: "github",
displayTitle: displayTitleFor(identity),
statusKey: "not_found",
statusLabel: "Not found",
statusIconKey: "archive",
statusCategory: "archived",
statusTone: "muted",
isTerminal: true,
@@ -217,9 +224,18 @@ function pullRequestSnapshot(identity: GitHubObjectIdentity, body: Record<string
}
return {
displayKey: displayKeyFor(identity),
iconKey: "github",
displayTitle: title ? `${displayTitleFor(identity)}: ${title}` : displayTitleFor(identity),
statusKey,
statusLabel,
statusIconKey: merged
? "git-merge"
: state === "closed"
? "x-circle"
: draft
? "clock"
: "git-pull-request",
statusCategory,
statusTone,
isTerminal,
@@ -257,9 +273,12 @@ function issueSnapshot(identity: GitHubObjectIdentity, body: Record<string, unkn
: "Unknown";
return {
displayKey: displayKeyFor(identity),
iconKey: "github",
displayTitle: title ? `${displayTitleFor(identity)}: ${title}` : displayTitleFor(identity),
statusKey,
statusLabel,
statusIconKey: state === "closed" ? "circle" : "circle-dot",
statusCategory: state === "open" ? "open" : state === "closed" ? "closed" : "unknown",
statusTone: state === "open" ? "info" : state === "closed" ? "muted" : "neutral",
isTerminal: state === "closed",
@@ -320,6 +339,8 @@ export function createGitHubExternalObjectProvider(
providerKey: "github",
objectType: identity.objectType,
externalId: externalIdFor(identity),
displayKey: displayKeyFor(identity),
iconKey: "github",
displayTitle: displayTitleFor(identity),
confidence: "exact",
}];