Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
302 changes: 302 additions & 0 deletions engine/app/assets/stylesheets/coplan/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -2977,6 +2977,296 @@ body:not(:has(.comment-toolbar)) .web-push-banner {
color: var(--color-text-muted);
}

.landing__footer-oss {
margin: var(--space-sm) 0 0;
}

.landing__kicker {
display: inline-block;
margin: 0 0 var(--space-md);
padding: 0.25rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: 999px;
background: var(--color-surface);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-text-muted);
letter-spacing: 0.01em;
}

.landing__features {
margin-bottom: var(--space-2xl);
}

.landing__feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: var(--space-lg);
}

.landing__feature {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: var(--space-lg);
}

.landing__feature h3 {
font-size: var(--text-base);
font-weight: 600;
margin: 0 0 var(--space-sm);
color: var(--color-text);
}

.landing__feature p {
margin: 0;
font-size: var(--text-sm);
line-height: 1.6;
color: var(--color-text-muted);
}

/* Copy-to-clipboard URL chip — shared partial coplan/shared/_copy_url */
.copy-url {
display: flex;
align-items: stretch;
gap: var(--space-sm);
flex-wrap: wrap;
}

.copy-url__value {
flex: 1 1 16rem;
display: flex;
align-items: center;
padding: 0.4rem 0.75rem;
background: var(--color-code-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius);
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--color-text);
overflow-wrap: anywhere;
}

.landing__agents .copy-url {
margin-top: var(--space-md);
}

/* Landing mock — an illustrative, CSS-only rendering of the review loop
(anchored comment → agent edit → new version). Uses the same design tokens
as the real plan UI so it stays truthful in both light and dark themes. */
.landing-mock {
display: grid;
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
gap: var(--space-lg);
align-items: start;
margin-bottom: var(--space-2xl);
}

.landing-mock__doc {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}

.landing-mock__doc-header {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-md) var(--space-lg);
border-bottom: 1px solid var(--color-border);
background: var(--color-surface-muted);
}

.landing-mock__doc-title {
font-weight: 600;
font-size: var(--text-sm);
color: var(--color-text);
margin-right: auto;
}

.landing-mock__badge {
padding: 0.1rem 0.5rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
}

.landing-mock__badge--status {
background: var(--color-status-considering-bg);
color: var(--color-status-considering);
}

.landing-mock__badge--version {
background: var(--color-tag-bg);
color: var(--color-tag-text);
}

.landing-mock__doc-body {
padding: var(--space-lg);
}

.landing-mock__heading {
margin: 0 0 var(--space-sm);
font-size: var(--text-base);
font-weight: 600;
color: var(--color-text);
}

.landing-mock__line {
margin: 0 0 var(--space-sm);
font-size: var(--text-sm);
line-height: 1.7;
color: var(--color-text);
}

.landing-mock__line--dim {
color: var(--color-text-muted);
}

.landing-mock__highlight {
background: var(--color-highlight-pending-bg);
border-bottom: 2px solid var(--color-highlight-pending-border);
color: inherit;
padding: 0 0.1em;
border-radius: 2px;
}

.landing-mock__thread {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}

.landing-mock__comment {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: var(--space-md);
}

.landing-mock__comment p {
margin: 0;
font-size: var(--text-sm);
line-height: 1.6;
color: var(--color-text);
}

.landing-mock__comment--agent {
border-color: var(--color-agent-bg);
background: var(--color-surface-muted);
margin-left: var(--space-lg);
}

.landing-mock__author {
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text-muted);
margin-bottom: var(--space-xs);
}

.landing-mock__agent-chip {
display: inline-block;
margin-left: 0.25rem;
padding: 0 0.4rem;
border-radius: 999px;
background: var(--color-agent-bg);
color: var(--color-agent);
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
}

.landing-mock__provenance {
margin-top: var(--space-sm);
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--color-text-muted);
}

/* Copy-to-clipboard button feedback (see clipboard_controller.js) */
.copy-button--copied {
background: var(--color-success);
border-color: var(--color-success);
color: var(--color-text-inverse);
}

/* Agent instructions — human-friendly HTML rendering at /agent-instructions.
Agents and curl still get raw markdown from the same URL; this page is what
browsers see. */
.agent-instructions {
max-width: 52rem;
margin: 0 auto;
padding: var(--space-2xl) var(--space-lg);
}

.agent-instructions__header {
margin-bottom: var(--space-2xl);
}

.agent-instructions__title {
font-size: 2rem;
font-weight: 700;
line-height: 1.2;
margin: 0 0 var(--space-md);
color: var(--color-text);
}

.agent-instructions__lede {
font-size: var(--text-lg);
line-height: 1.6;
color: var(--color-text-muted);
margin: 0 0 var(--space-xl);
}

.agent-instructions__url-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: var(--space-lg);
margin-bottom: var(--space-lg);
}

.agent-instructions__url-label {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-text-muted);
margin-bottom: var(--space-sm);
}

.agent-instructions__fetch-hint {
font-size: var(--text-sm);
color: var(--color-text-muted);
line-height: 1.6;
margin: 0 0 var(--space-sm);
}

.agent-instructions__curl {
margin: 0 0 var(--space-sm);
padding: var(--space-md);
background: var(--color-code-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius);
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--color-text);
overflow-x: auto;
}

.agent-instructions__raw-link {
color: var(--color-primary);
text-decoration: underline;
}

.agent-instructions__doc {
border-top: 1px solid var(--color-border);
padding-top: var(--space-xl);
}

@media (max-width: 640px) {
.landing {
padding: var(--space-xl) var(--space-md);
Expand All @@ -2985,6 +3275,18 @@ body:not(:has(.comment-toolbar)) .web-push-banner {
.landing__title {
font-size: 1.75rem;
}

.landing-mock {
grid-template-columns: 1fr;
}

.landing-mock__comment--agent {
margin-left: var(--space-md);
}

.agent-instructions {
padding: var(--space-xl) var(--space-md);
}
}

/* Stale-content banner — shown when the plan body was updated in another tab
Expand Down
51 changes: 50 additions & 1 deletion engine/app/controllers/coplan/agent_instructions_controller.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,26 @@
module CoPlan
# Serves the agent API instructions at /agent-instructions.
#
# This endpoint has two audiences:
#
# * **Agents and CLIs** (curl, HTTP libraries, coding agents) fetch it as raw
# Markdown. Every API response points here via the `X-Agent-Instructions`
# header, so the raw behavior is load-bearing: any client that does not
# explicitly ask for HTML gets `text/markdown`, byte-identical to what this
# endpoint has always served.
# * **Humans in a browser** (e.g. clicking the link on the landing page) get
# the same instructions rendered as a styled HTML page, with the raw URL
# front-and-center so they can hand it to their agent.
#
# Negotiation is deliberately conservative: we only serve HTML when the
# Accept header *leads* with `text/html`, which is exactly what every
# browser sends (`Accept: text/html,application/xhtml+xml,…`) and what
# Turbo Drive sends on navigation (`Accept: text/html, application/xhtml+xml`).
# curl's default `Accept: */*`, an absent Accept header, or
# `Accept: text/markdown` all fall through to raw Markdown. An explicit
# format always wins over the Accept header: `/agent-instructions.md`
# forces raw Markdown (so browsers can view the source document) and
# `/agent-instructions.html` forces the rendered page.
class AgentInstructionsController < ApplicationController
skip_before_action :authenticate_coplan_user!

Expand All @@ -11,7 +33,34 @@ def show
# carries the request's SCRIPT_NAME.
@base = "#{request.base_url}#{root_path.chomp("/")}"
@plan_types = PlanType.order(:name)
render layout: false, content_type: "text/markdown", formats: [:text]

if prefers_html?
# The page is public, but signed-in visitors should still see their
# normal nav chrome (search, inbox, sign-out) in the shared layout —
# same optional-resolve approach as WelcomeController.
@current_coplan_user = CoPlan::Authentication.user_from_request(request)
CoPlan::Current.user = current_user

@instructions_url = coplan.agent_instructions_url
@instructions_markdown = render_to_string(:show, formats: [:text], layout: false)
render :show, formats: [:html]
else
render layout: false, content_type: "text/markdown", formats: [:text]
end
end

private

def prefers_html?
return true if params[:format] == "html"
return false if params[:format].present?

# Intentionally a string check on the raw header rather than
# `request.format`/`request.accepts`: Rails maps curl's `*/*` to HTML,
# which would break every agent that follows X-Agent-Instructions here.
# Requiring the header to lead with text/html matches browsers exactly
# and nothing else.
request.headers["Accept"].to_s.strip.start_with?("text/html")
end
end
end
Loading
Loading