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
11 changes: 10 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,16 @@ These are how the user wants Claude to behave when working on this repo:
in chat (often via .env reminders the harness sends), warn them once
that it's now in conversation history and tell them to rotate.

8. **GitHub comments use a standard signature.** You're authorized to leave
8. **Don't put boxes around screenshots on the marketing site.** No
`border`, no `border-radius` wrapper, no glow `box-shadow` framing.
The dark background on `site/public/*.html` is already the frame;
adding a border to an `<img>` makes it look enclosed in a card it
isn't part of. Plain `<img>` (width 100%, `display: block`) is the
right default. The user has had to undo this on multiple sessions —
if a task brief asks for a border around a screenshot, push back
before shipping it.

9. **GitHub comments use a standard signature.** You're authorized to leave
comments on issues and PRs (via the `gh` CLI or the GitHub REST/GraphQL
API) without re-confirming each time, provided the comment ends with a
one-line signature so readers know the comment came from an agent acting
Expand Down
219 changes: 219 additions & 0 deletions site/public/announcements.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Announcements — Harness</title>
<meta name="description" content="News from the Harness team. New features, releases, and changes worth reading about." />

<!-- Open Graph -->
<meta property="og:site_name" content="Harness" />
<meta property="og:title" content="Announcements — Harness" />
<meta property="og:description" content="News from the Harness team. New features, releases, and changes worth reading about." />
<meta property="og:image" content="https://harness.mikelyons.org/og-image.png" />
<meta property="og:url" content="https://harness.mikelyons.org/announcements.html" />
<meta property="og:type" content="website" />

<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Announcements — Harness" />
<meta name="twitter:description" content="News from the Harness team. New features, releases, and changes worth reading about." />
<meta name="twitter:image" content="https://harness.mikelyons.org/og-image.png" />

<link rel="icon" type="image/png" href="/icon.png" />
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />

<style>
html, body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0a0a; color: #e5e5e5; }
code { font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace; }
.gradient-text { background: linear-gradient(135deg, #f59e0b 0%, #ef4444 50%, #a855f7 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.grid-bg {
background-image:
radial-gradient(circle at 50% 0%, rgba(245, 158, 11, 0.08), transparent 50%),
radial-gradient(circle at 0% 100%, rgba(168, 85, 247, 0.06), transparent 50%);
}
.post-card {
background: rgba(10, 10, 10, 0.6);
border: 1px solid #1f1f1f;
border-radius: 0.875rem;
padding: 1.5rem 1.75rem;
transition: border-color 0.15s, background 0.15s;
display: block;
color: inherit;
text-decoration: none;
}
.post-card:hover {
border-color: #404040;
background: rgba(15, 15, 15, 0.8);
}
.post-card .date {
font-size: 0.75rem;
color: #737373;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 0.5rem;
}
.post-card h2 {
font-size: 1.5rem;
font-weight: 700;
color: #f5f5f5;
margin-bottom: 0.5rem;
line-height: 1.25;
}
.post-card:hover h2 {
color: #fbbf24;
}
.post-card p {
color: #a3a3a3;
line-height: 1.6;
margin: 0;
}
.post-card .arrow {
display: inline-flex;
align-items: center;
gap: 0.375rem;
margin-top: 0.875rem;
font-size: 0.8125rem;
color: #737373;
transition: color 0.15s, transform 0.15s;
}
.post-card:hover .arrow {
color: #fbbf24;
}
.empty-state {
border: 1px dashed #262626;
border-radius: 0.875rem;
padding: 3rem 1.5rem;
text-align: center;
color: #737373;
font-size: 0.9375rem;
}
</style>
</head>
<body class="min-h-screen grid-bg">

<!-- Nav -->
<nav class="max-w-6xl mx-auto px-6 py-6 flex items-center justify-between">
<a href="/" class="flex items-center gap-3">
<img src="/icon.png" alt="Harness icon" class="w-9 h-9 rounded-lg" />
<span class="text-xl font-bold">Harness</span>
</a>
<div class="flex items-center gap-4 sm:gap-6 text-sm text-neutral-400">
<a href="/#features" class="hidden sm:inline hover:text-neutral-100 transition-colors">Features</a>
<a href="/guide.html" class="hover:text-neutral-100 transition-colors">Guide</a>
<a href="/announcements.html" class="text-neutral-100 transition-colors">Announcements</a>
<a href="/#install" class="hover:text-neutral-100 transition-colors">Install</a>
<a href="https://github.com/frenchie4111/harness" class="hover:text-neutral-100 transition-colors">GitHub</a>
</div>
</nav>

<!-- Header -->
<section class="max-w-3xl mx-auto px-6 pt-12 pb-12">
<div class="text-xs uppercase tracking-widest text-neutral-500 mb-4">News</div>
<h1 class="text-5xl md:text-6xl font-extrabold tracking-tight mb-6 leading-tight">
<span class="gradient-text">Announcements.</span>
</h1>
<p class="text-lg text-neutral-400 leading-relaxed">
A modern chat interface for Claude Code, built into Harness.
Same agent, same auth, same MCP setup.
</p>
</section>

<!-- Posts -->
<main class="max-w-3xl mx-auto px-6 pb-24">
<div id="posts-list" class="space-y-4">
<div id="posts-loading" class="empty-state">Loading announcements…</div>
</div>
</main>

<!-- Footer -->
<footer class="border-t border-neutral-900 mt-20">
<div class="max-w-6xl mx-auto px-6 py-10 flex flex-col md:flex-row items-center justify-between gap-4 text-sm text-neutral-600">
<div class="flex items-center gap-2">
<img src="/icon.png" alt="" class="w-5 h-5 rounded" />
<span>Harness · Open source · MIT</span>
</div>
<div class="flex items-center gap-6">
<a href="/guide.html" class="hover:text-neutral-300 transition-colors">Guide</a>
<a href="/announcements.html" class="text-neutral-300 transition-colors">Announcements</a>
<a href="https://github.com/frenchie4111/harness" class="hover:text-neutral-300 transition-colors">GitHub</a>
<a href="/releases.html" class="hover:text-neutral-300 transition-colors">Release notes</a>
<a href="https://github.com/frenchie4111/harness/releases" class="hover:text-neutral-300 transition-colors">Downloads</a>
</div>
</div>
</footer>

<script type="module">
// Data-driven render: each new announcement only needs an entry in
// /announcements.json (with optional `summary`) and the per-post HTML
// under /announcements/<slug>.html. The desktop-app feed validator
// ignores fields it doesn't know about, so `summary` is safe to add.
const listEl = document.getElementById('posts-list')
const loadingEl = document.getElementById('posts-loading')

const dateFmt = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC'
})

function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}

function render(items) {
loadingEl?.remove()
if (!items.length) {
listEl.innerHTML =
'<div class="empty-state">No announcements yet. Check back soon.</div>'
return
}
const sorted = [...items].sort(
(a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt)
)
listEl.innerHTML = sorted
.map((a) => {
const date = Number.isNaN(Date.parse(a.publishedAt))
? ''
: dateFmt.format(new Date(a.publishedAt))
const summary = a.summary
? `<p>${escapeHtml(a.summary)}</p>`
: ''
return `
<a class="post-card" href="${escapeHtml(a.href)}">
<div class="date">${escapeHtml(date)}</div>
<h2>${escapeHtml(a.title)}</h2>
${summary}
<span class="arrow">
Read more
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
</span>
</a>
`
})
.join('')
}

fetch('/announcements.json', { cache: 'no-cache' })
.then((r) => (r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status))))
.then((data) => {
const items = Array.isArray(data?.announcements) ? data.announcements : []
render(items)
})
.catch((err) => {
loadingEl?.remove()
listEl.innerHTML =
'<div class="empty-state">Couldn\'t load announcements. Try refreshing.</div>'
// eslint-disable-next-line no-console
console.error('Failed to load announcements:', err)
})
</script>
</body>
</html>
10 changes: 9 additions & 1 deletion site/public/announcements.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
{
"announcements": []
"announcements": [
{
"id": "introducing-chat-mode-2026-05",
"title": "Introducing Chat mode",
"href": "https://harness.mikelyons.org/announcements/introducing-chat-mode.html",
"publishedAt": "2026-05-25",
"summary": "A modern chat interface for Claude Code, built into Harness. Same agent, same auth, same MCP setup."
}
]
}
Binary file added site/public/announcements/img/chat-mode.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/public/announcements/img/terminal-mode.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading