research: publishing → published + artifact download (R1)
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 10s
ci / frontend (push) Successful in 26s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped

Before this commit, approve_publish left topics stuck in 'publishing'
forever — the sidebar 'published' bucket was always empty and nothing
surfaced the artifact. Two-part fix:

State machine — approve_publish now transitions reviewing → publishing
→ published in one API call. Real async packaging isn't a thing yet
because the artifact IS the markdown already written to
research_outcomes when the last run completed (topology_worker). The
intermediate 'publishing' state is preserved (schema-level trigger
stamps published_at on landing there) so we keep the option to detour
through it later for pdf render / mirror-to-store / etc.

Download endpoint — GET /api/research/:id/artifact returns the latest
outcome as text/markdown with Content-Disposition: attachment. Filename
sanitizes the topic title to ascii-alnum + dash and appends the outcome
version so accumulated revision drafts (post R2) don't clobber.
Workspace ownership check via research_topics::get; 404 if no outcome
yet (reviewers browsing before a run completes).

Frontend — ResearchList shows a green Download icon-button next to
Delete when status === 'published'. Clicks trigger a plain anchor
download of the .md — no JS blob dance needed since the response is
already an attachment.

Follow-up queued: pdf render (server-side or client-side of the md),
mirror-to-store (S3-ish or Obsidian vault) as an async step during the
publishing→published detour.
This commit is contained in:
Omar Sobh
2026-07-09 14:05:21 -07:00
parent f60df36717
commit f9e8d8d779
3 changed files with 83 additions and 6 deletions
@@ -6,7 +6,7 @@
// outcomes; topology_runs.research_topic_id is SET NULL).
import { useEffect, useState } from "react";
import { Plus, Trash2 } from "lucide-react";
import { Download, Plus, Trash2 } from "lucide-react";
import type { Agent } from "@/lib/api/schemas";
import {
@@ -316,6 +316,28 @@ export function ResearchList({
marginTop: 2,
}}
>
{t.status === "published" ? (
<a
href={`/api/research/${t.id}/artifact`}
title="Download artifact (.md)"
aria-label="Download artifact"
style={{
width: 24,
height: 24,
borderRadius: 6,
border: "1px solid rgba(255,255,255,.08)",
background: "transparent",
color: "#6fd0c0",
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
textDecoration: "none",
}}
>
<Download aria-hidden size={12} />
</a>
) : null}
<button
type="button"
title="Delete topic"