diff --git a/src/projections/activity.ts b/src/projections/activity.ts index ce9414d..af2d9a0 100644 --- a/src/projections/activity.ts +++ b/src/projections/activity.ts @@ -24,10 +24,21 @@ export interface ActivityItem { parentEventId?: string; privacy: string; summary: string; + presentation: ActivityPresentation; descendantEventCount: number; consumerRuns: ActivityConsumerRun[]; } +export interface ActivityPresentation { + title: string; + body: string; + objectLabel: string; + url?: string; + urlLabel?: string; + parentUrl?: string; + parentLabel?: string; +} + export interface RootActivityProjection { totalEvents: number; byType: Record; @@ -75,6 +86,7 @@ export async function buildRootActivity(store: JazzThoughtStore, limit = 100): P ...(event.parentEventId ? { parentEventId: event.parentEventId } : {}), privacy: event.privacy, summary: summarizeRoot(event), + presentation: presentRoot(event), descendantEventCount: descendantsByRoot.get(event.id) ?? 0, consumerRuns: (runsByRoot.get(event.id) ?? []).map((run) => ({ id: run.id, @@ -135,6 +147,7 @@ export async function buildRecentRootActivity( ...(event.parentEventId ? { parentEventId: event.parentEventId } : {}), privacy: event.privacy, summary: summarizeRoot(event), + presentation: presentRoot(event), descendantEventCount: descendantsByRoot.get(event.id)?.size ?? 0, consumerRuns: (runsByRoot.get(event.id) ?? []).map((run) => ({ id: run.id, @@ -214,6 +227,58 @@ function summarizeRoot(event: ThoughtEvent): string { return event.type; } +function presentRoot(event: ThoughtEvent): ActivityPresentation { + if (event.type === "stream.thought.source.atproto.commit") { + const payload = event.payload; + const operation = typeof payload.operation === "string" ? payload.operation : "observed"; + const record = isObject(payload.record) ? payload.record : undefined; + const collection = typeof payload.collection === "string" ? payload.collection : undefined; + const atUri = typeof payload.atUri === "string" ? payload.atUri : undefined; + if (collection === "app.bsky.feed.post") { + const reply = record && isObject(record.reply) && isObject(record.reply.parent) + ? typeof record.reply.parent.uri === "string" ? record.reply.parent.uri : undefined + : undefined; + const deleted = operation === "delete"; + return { + title: deleted ? "Deleted a Bluesky post" : reply ? "Replied on Bluesky" : "Posted on Bluesky", + body: record && typeof record.text === "string" ? record.text : "", + objectLabel: deleted ? "Post removed from Bluesky" : "Bluesky post", + ...(atUri ? { url: atUriToWebUrl(atUri), urlLabel: reply ? "Open reply" : "Open post" } : {}), + ...(reply ? { parentUrl: atUriToWebUrl(reply), parentLabel: "Open parent post" } : {}), + }; + } + if (collection === "app.bsky.feed.like" || collection === "app.bsky.feed.repost") { + const subject = record && isObject(record.subject) && typeof record.subject.uri === "string" + ? record.subject.uri + : undefined; + const like = collection.endsWith("like"); + const removed = operation === "delete"; + return { + title: like ? removed ? "Removed a like" : "Liked a post" : removed ? "Removed a repost" : "Reposted on Bluesky", + body: "", + objectLabel: "Bluesky post", + ...(subject ? { url: atUriToWebUrl(subject), urlLabel: like && !removed ? "Open liked post" : "Open post" } : {}), + }; + } + if (collection === "app.bsky.graph.follow") { + const subject = record && typeof record.subject === "string" ? record.subject : undefined; + const removed = operation === "delete"; + return { + title: removed ? "Stopped following an account" : "Followed an account", + body: "", + objectLabel: "Bluesky account", + ...(subject ? { url: `https://bsky.app/profile/${encodeURIComponent(subject)}`, urlLabel: "Open profile" } : {}), + }; + } + } + const summary = summarizeRoot(event); + return { + title: event.type === "stream.thought.source.x.activity" ? "X activity" : capitalize(event.type.split(".").at(-1) ?? "observation"), + body: summary === event.type ? "" : summary, + objectLabel: "Observation", + }; +} + function runKind(run: AgentRun): ActivityConsumerRun["kind"] { return run.provider === "deterministic" && run.model === "deterministic" ? "rule" : "model"; } diff --git a/src/web/inspector.ts b/src/web/inspector.ts index 8545052..4ef4655 100644 --- a/src/web/inspector.ts +++ b/src/web/inspector.ts @@ -561,6 +561,7 @@ export function renderInspectorHtml(): string { .spinner { display:inline-block; width:10px; height:10px; margin-right:7px; border:1px solid var(--line); border-top-color:var(--cyan); border-radius:50%; animation:spin .8s linear infinite; vertical-align:-1px } .retry { margin-top:12px; border:1px solid var(--red); background:transparent; color:var(--text); padding:7px 10px; font:inherit; cursor:pointer } .observation-head h2 { margin-bottom:4px; font-size:17px } .observation-card { margin:14px 0; border:1px solid #30404a; background:#0e151a; padding:14px 15px; font:15px/1.55 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; overflow-wrap:anywhere } .observation-card p { margin:0; white-space:pre-wrap } .observation-links { display:flex; flex-wrap:wrap; gap:8px; margin-top:12px } .observation-links a { border:1px solid #35515c; color:var(--cyan); padding:6px 9px; text-decoration:none; font:11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace } .processing-state { display:flex; align-items:center; gap:8px; margin:14px 0 4px; color:var(--muted); font-size:12px } .status-dot { width:7px; height:7px; border-radius:50%; background:#4b5962; flex:none } .technical-record { margin-top:22px } .technical-record > summary { padding:2px 0 7px } .technical-section { margin-top:15px } .technical-section h3 { margin-top:0 } + .source-event-list { display:grid; gap:7px; margin:8px 0 16px } .source-event { width:100%; border:1px solid var(--line); border-left:2px solid var(--cyan); background:#0d1216; color:var(--text); padding:10px 11px; text-align:left; font:inherit; cursor:pointer } .source-event:hover { background:#10191f } .source-event .source-event-title { color:var(--cyan); margin-bottom:3px } .source-event .source-event-body { overflow:hidden; display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; font:13px/1.45 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif } .source-overview { margin-top:12px } .source-state { border-top:2px solid #35515c } .source-state.good { border-top-color:var(--cyan) } .source-state.waiting { border-top-color:var(--amber) } .filter-bar { display:grid; grid-template-columns:minmax(120px,1fr) minmax(110px,.85fr) minmax(110px,.7fr) minmax(150px,1.25fr) auto; gap:6px; padding:8px 10px; border-bottom:1px solid var(--line); background:#0d1115 } .filter-bar[hidden] { display:none } .filter-bar select,.filter-bar input { min-width:0; height:30px; padding:4px 7px; font-size:11px } .filter-bar button { border:1px solid var(--line); background:transparent; color:var(--muted); padding:4px 8px; font:11px/1.4 inherit; cursor:pointer } .filter-bar button:hover { color:var(--text); border-color:#40515d } @keyframes spin { to { transform:rotate(360deg) } } @media (prefers-reduced-motion:reduce) { .spinner { animation:none; border-color:var(--cyan) } } @@ -627,7 +628,7 @@ function sourceItem(source){ function suggestionItem(item){return ''} function reviewItem(item){const status=item.decision?item.decision.disposition:'unreviewed';return ''} async function select(kind,id){ - if(kind==='source'){const source=state.snapshot.sources.find(value=>value.source===id);document.querySelector('#detail').innerHTML=renderSource(source);return} + if(kind==='source'){const source=state.snapshot.sources.find(value=>value.source===id);const detail=document.querySelector('#detail');detail.innerHTML=renderSource(source);detail.querySelectorAll('[data-event-id]').forEach(button=>button.addEventListener('click',()=>select('event',button.dataset.eventId)));return} if(kind==='suggestion'){const item=state.proposals.items.find(value=>value.eventId===id);document.querySelector('#detail').innerHTML=renderSuggestion(item);bindSuggestionForm(item);return} if(kind==='review'){const item=state.reviews.items.find(value=>value.id===id);document.querySelector('#detail').innerHTML=renderReview(item);bindReviewForm(item);return} if(kind==='artifact'){state.selected={kind,id};document.querySelector('main').classList.add('artifact-selected');document.querySelector('#detail').innerHTML='
Loading artifact…
';bindArtifactBack();const response=await fetch('api/artifacts/'+encodeURIComponent(id));const data=await response.json();document.querySelector('#detail').innerHTML=renderArtifact(data);bindArtifactBack();return} @@ -641,6 +642,7 @@ function sourceLabelFromId(source){if(source==='jetstream:cameron-atproto')retur function observationSourceLabel(event){return sourceLabelFromId(event?.source)} function humanPrivacy(value){return value==='public-source'?'public source':value==='sensitive'?'sensitive':'private'} function observationPresentation(event){ + if(event?.presentation)return {...event.presentation,sourceLabel:observationSourceLabel(event),url:event.presentation.url??'',urlLabel:event.presentation.urlLabel??'',parentUrl:event.presentation.parentUrl??'',parentLabel:event.presentation.parentLabel??''}; const payload=event?.payload??{};const record=payload.record??{};const operation=payload.operation??'observed'; if(event?.type==='stream.thought.source.atproto.commit'){ if(payload.collection==='app.bsky.feed.post'){ @@ -690,10 +692,19 @@ function isModelRun(run){return !(run.provider==='deterministic'&&run.model==='d function renderSource(source){ if(!source)return '
Source not found.
'; const runtime=source.control?.runtime;const upstream=source.control?.upstream; - const facts='
Configuration: '+(source.configured?(source.enabled?'enabled':'disabled'):'not registered')+'Runtime: '+(runtime?(runtime.current?'ready':'stale'):'not recorded')+'Provider: '+(upstream?(upstream.registered&&upstream.valid?'registered':'unavailable'):'not applicable / unrecorded')+'Subscriptions: '+(upstream?(upstream.liveSubscriptionCount+'/'+upstream.desiredSubscriptionCount+(upstream.subscriptionsConverged?' converged':' drifting')):'not recorded')+'Activity: '+(source.hasActivityEvidence?source.status:'none yet')+'
'; + const recent=(state.snapshot?.activity?.items??[]).filter(event=>event.source===source.source).slice(0,8); + const recentEvents=recent.length?'
'+recent.map(event=>{const observation=observationPresentation(event);return ''}).join('')+'
':'
No durable observations from this source yet.
'; + const configuration=source.configured?(source.enabled?'Enabled':'Disabled'):'Observed only'; + const runtimeState=runtime?(runtime.current?'Ready':'Stale'):'Not recorded'; + const providerState=upstream?(upstream.registered&&upstream.valid?'Registered':'Unavailable'):'Not applicable'; + const subscriptionState=upstream?upstream.liveSubscriptionCount+'/'+upstream.desiredSubscriptionCount+(upstream.subscriptionsConverged?' converged':' drifting'):'Not recorded'; + const activityState=source.hasActivityEvidence?humanLabel(source.status):'Awaiting first activity'; + const lastActivity=source.lastSuccessAt?new Date(source.lastSuccessAt).toLocaleString():'None recorded'; + const cards='
Configuration
'+esc(configuration)+'
Runtime
'+esc(runtimeState)+'
Provider
'+esc(providerState)+'
Subscriptions
'+esc(subscriptionState)+'
Data plane
'+esc(activityState)+'
Last completed activity
'+esc(lastActivity)+'
'; const control={configured:source.configured,enabled:source.enabled,kind:source.kind,lane:source.control?.lane,runtime:runtime?{state:runtime.state,current:runtime.current,readyAt:runtime.readyAt,revision:runtime.revision}:undefined,upstream:upstream?{registered:upstream.registered,valid:upstream.valid,current:upstream.current,webhookId:upstream.webhookId,subscriptions:upstream.liveSubscriptionCount+'/'+upstream.desiredSubscriptionCount,subscriptionsConverged:upstream.subscriptionsConverged,checkedAt:upstream.checkedAt}:undefined}; const evidence={status:source.status,hasActivityEvidence:source.hasActivityEvidence,operationsStarted:source.operationsStarted,operationsCompleted:source.operationsCompleted,operationFailures:source.operationFailures,recoveries:source.recoveries,inFlight:source.inFlight,lastSuccessAt:source.lastSuccessAt,lastFailureAt:source.lastFailureAt,lastError:source.lastError,cursorId:source.cursorId,cursor:source.cursor,updatedAt:source.updatedAt}; - return '

'+esc(source.source)+'

Control-plane registration and data-plane evidence are independent receipts.
'+facts+'

control plane

'+json(control)+'

data-plane evidence

'+json(evidence)+'
'; + const technical='
Technical details

Source ID

'+esc(source.source)+'

Control plane

'+json(control)+'

Data-plane evidence

'+json(evidence)+'
'; + return '

'+esc(sourceLabelFromId(source.source))+'

'+esc(source.source)+'

Recent observations

'+recentEvents+'

Connection

'+cards+'
Configuration, runtime readiness, provider registration, and delivered activity are independent receipts.
'+technical; } function renderSuggestion(item){ if(!item)return '
Suggestion not found.
'; diff --git a/test/atproto-observer.test.ts b/test/atproto-observer.test.ts index 14d21fe..100ebe2 100644 --- a/test/atproto-observer.test.ts +++ b/test/atproto-observer.test.ts @@ -62,6 +62,13 @@ describe("Bluesky activity observer", () => { const activity = await buildRootActivity(store); expect(activity.items.find((item) => item.id === ingest.events[0]?.id)).toMatchObject({ summary: "Create post: A public post with a link", + presentation: { + title: "Posted on Bluesky", + body: "A public post with a link", + objectLabel: "Bluesky post", + url: "https://bsky.app/profile/did:plc:alicefixture/post/post-one", + urlLabel: "Open post", + }, descendantEventCount: 4, consumerRuns: [{ agentId: "bluesky-activity-observer", @@ -71,6 +78,42 @@ describe("Bluesky activity observer", () => { }], }); }); + + test("projects a Bluesky like as a useful list label without exposing the raw payload", async () => { + const project = await temporaryProject(); + roots.push(project); + const store = testStore(project); + stores.push(store); + const connector = new JetstreamConnector({ id: "jetstream:cameron-atproto", collections: ["app.bsky.feed.like"] }); + const ingest = await connector.ingestBatch(store, [{ + did: "did:plc:cameronfixture", + time_us: 1786405008000000, + kind: "commit", + commit: { + rev: "3mfixturelike", + operation: "create", + collection: "app.bsky.feed.like", + rkey: "like-one", + cid: "bafyfixturelikeone", + record: { + $type: "app.bsky.feed.like", + subject: { uri: "at://did:plc:authorfixture/app.bsky.feed.post/post-one", cid: "bafyfixturepost" }, + createdAt: "2026-08-10T22:36:48.000Z", + }, + }, + }]); + + const activity = await buildRootActivity(store); + expect(activity.items.find((item) => item.id === ingest.events[0]?.id)).toMatchObject({ + presentation: { + title: "Liked a post", + body: "", + objectLabel: "Bluesky post", + url: "https://bsky.app/profile/did:plc:authorfixture/post/post-one", + urlLabel: "Open liked post", + }, + }); + }); }); async function waitFor(predicate: () => Promise, timeoutMs = 2_000): Promise {