feat: add website release notes
This commit is contained in:
parent
4ec4d56fcf
commit
7ebc72f3f5
|
|
@ -5,12 +5,13 @@ usage() {
|
|||
cat <<'USAGE'
|
||||
usage: scripts/seed_navigator_demo.sh [--allow-main]
|
||||
|
||||
Seeds a running herdr dev server with navigator demo workspaces, tabs, panes,
|
||||
and fake agent states. Most panes are intentionally unnamed.
|
||||
Seeds a running herdr server with navigator demo workspaces, tabs, panes,
|
||||
and fake agent states for recording the session navigator.
|
||||
|
||||
Environment:
|
||||
HERDR_NAV_SOCKET_PATH API socket to target. Defaults to $HOME/.config/herdr-dev/herdr.sock.
|
||||
HERDR_NAV_CWD Workspace cwd for created panes. Defaults to the repo root.
|
||||
HERDR_NAV_BIN Herdr binary to call. Defaults to cargo run from the repo.
|
||||
USAGE
|
||||
}
|
||||
|
||||
|
|
@ -55,7 +56,11 @@ fi
|
|||
|
||||
cd "$repo_dir"
|
||||
|
||||
run() { cargo run --quiet -- "$@"; }
|
||||
if [[ -n "${HERDR_NAV_BIN:-}" ]]; then
|
||||
run() { "$HERDR_NAV_BIN" "$@"; }
|
||||
else
|
||||
run() { cargo run --quiet -- "$@"; }
|
||||
fi
|
||||
|
||||
mkws() {
|
||||
local label="$1"
|
||||
|
|
@ -90,74 +95,62 @@ report() {
|
|||
--seq "$seq" >/dev/null
|
||||
}
|
||||
|
||||
stamp="$(date +%H%M%S)"
|
||||
done_panes=()
|
||||
|
||||
read WS1 P1 T1 < <(mkws "nav-${stamp}-claude-review")
|
||||
P2="$(split "$P1" down)"
|
||||
P3="$(split "$P1" right)"
|
||||
rename_sparse "$P2" "approval needed"
|
||||
report "$P1" claude blocked blocked 1
|
||||
report "$P2" claude working working 1
|
||||
# P3 stays shell/unknown.
|
||||
read API_WS API_CODEX API_TAB < <(mkws api)
|
||||
run tab rename "$API_TAB" agents >/dev/null
|
||||
API_SHELL="$(split "$API_CODEX" down)"
|
||||
API_CLAUDE="$(split "$API_CODEX" right)"
|
||||
read API_LOGS_TAB API_LOGS < <(mktab "$API_WS" logs)
|
||||
rename_sparse "$API_CODEX" "codex env review"
|
||||
rename_sparse "$API_CLAUDE" "claude api refactor"
|
||||
rename_sparse "$API_SHELL" "api shell"
|
||||
rename_sparse "$API_LOGS" "deploy logs"
|
||||
report "$API_CODEX" codex blocked "env approval" 1
|
||||
report "$API_CLAUDE" claude working "refactor api" 1
|
||||
report "$API_SHELL" shell idle ready 1
|
||||
report "$API_LOGS" deploy working "tail logs" 1
|
||||
|
||||
read WS2 P4 T2 < <(mkws "nav-${stamp}-codex-build")
|
||||
P5="$(split "$P4" right)"
|
||||
read T2B P6 < <(mktab "$WS2" tests)
|
||||
P7="$(split "$P6" down)"
|
||||
report "$P4" codex working working 1
|
||||
report "$P5" codex working working 1
|
||||
report "$P6" codex idle idle 1
|
||||
report "$P7" codex working working 1
|
||||
done_panes+=("$P7:codex")
|
||||
read WEB_WS WEB_PI WEB_TAB < <(mkws web)
|
||||
run tab rename "$WEB_TAB" agents >/dev/null
|
||||
WEB_CLAUDE="$(split "$WEB_PI" down)"
|
||||
WEB_CODEX="$(split "$WEB_PI" right)"
|
||||
read WEB_PREVIEW_TAB WEB_PREVIEW < <(mktab "$WEB_WS" preview)
|
||||
rename_sparse "$WEB_PI" "pi ui build"
|
||||
rename_sparse "$WEB_CODEX" "codex css review"
|
||||
rename_sparse "$WEB_CLAUDE" "claude approval"
|
||||
rename_sparse "$WEB_PREVIEW" "preview server"
|
||||
report "$WEB_PI" pi working "build ui" 1
|
||||
report "$WEB_CLAUDE" claude blocked "tool approval" 1
|
||||
report "$WEB_PREVIEW" server idle "preview ready" 1
|
||||
report "$WEB_CODEX" codex working "css pass" 1
|
||||
done_panes+=("$WEB_CODEX:codex:review ready")
|
||||
|
||||
read WS3 P8 T3 < <(mkws "nav-${stamp}-finished")
|
||||
P9="$(split "$P8" down)"
|
||||
P10="$(split "$P8" right)"
|
||||
rename_sparse "$P10" "release notes"
|
||||
report "$P8" claude working working 1
|
||||
report "$P9" claude working working 1
|
||||
report "$P10" codex idle idle 1
|
||||
done_panes+=("$P8:claude" "$P9:claude")
|
||||
read DOCS_WS DOCS_NOTES DOCS_TAB < <(mkws docs)
|
||||
run tab rename "$DOCS_TAB" release >/dev/null
|
||||
DOCS_COPY="$(split "$DOCS_NOTES" right)"
|
||||
rename_sparse "$DOCS_NOTES" "codex notes done"
|
||||
rename_sparse "$DOCS_COPY" "codex release copy"
|
||||
report "$DOCS_COPY" codex working "release copy" 1
|
||||
report "$DOCS_NOTES" codex working "checking notes" 1
|
||||
done_panes+=("$DOCS_NOTES:codex:notes done")
|
||||
|
||||
read WS4 P11 T4 < <(mkws "nav-${stamp}-quiet")
|
||||
read T4B P12 < <(mktab "$WS4" notes)
|
||||
P13="$(split "$P12" right)"
|
||||
report "$P12" pi idle idle 1
|
||||
# P11 and P13 stay shell/unknown.
|
||||
read INFRA_WS INFRA_HERMES INFRA_TAB < <(mkws infra)
|
||||
run tab rename "$INFRA_TAB" ops >/dev/null
|
||||
INFRA_SSH="$(split "$INFRA_HERMES" right)"
|
||||
rename_sparse "$INFRA_HERMES" "hermes ssh prompt"
|
||||
rename_sparse "$INFRA_SSH" "ssh monitor"
|
||||
report "$INFRA_HERMES" hermes blocked "ssh prompt" 1
|
||||
report "$INFRA_SSH" ssh idle connected 1
|
||||
|
||||
read WS5 P14 T5 < <(mkws "nav-${stamp}-mixed")
|
||||
P15="$(split "$P14" down)"
|
||||
P16="$(split "$P14" right)"
|
||||
read T5B P17 < <(mktab "$WS5" agents)
|
||||
P18="$(split "$P17" down)"
|
||||
rename_sparse "$P14" "prod decision"
|
||||
report "$P14" claude blocked blocked 1
|
||||
report "$P15" codex blocked blocked 1
|
||||
report "$P16" claude working working 1
|
||||
report "$P17" codex working working 1
|
||||
report "$P18" claude idle idle 1
|
||||
|
||||
read WS6 P19 T6 < <(mkws "nav-${stamp}-dense")
|
||||
P20="$(split "$P19" down)"
|
||||
P21="$(split "$P19" right)"
|
||||
read T6B P22 < <(mktab "$WS6" long-run)
|
||||
P23="$(split "$P22" down)"
|
||||
P24="$(split "$P22" right)"
|
||||
report "$P19" codex working working 1
|
||||
report "$P20" claude idle idle 1
|
||||
report "$P21" codex blocked blocked 1
|
||||
report "$P22" claude working working 1
|
||||
report "$P23" codex working working 1
|
||||
report "$P24" claude working working 1
|
||||
done_panes+=("$P24:claude")
|
||||
|
||||
run workspace focus "$WS1" >/dev/null
|
||||
run workspace focus "$API_WS" >/dev/null
|
||||
seq=2
|
||||
for item in "${done_panes[@]}"; do
|
||||
pane="${item%%:*}"
|
||||
agent="${item##*:}"
|
||||
report "$pane" "$agent" idle done "$seq"
|
||||
rest="${item#*:}"
|
||||
agent="${rest%%:*}"
|
||||
status="${rest#*:}"
|
||||
report "$pane" "$agent" idle "$status" "$seq"
|
||||
seq=$((seq + 1))
|
||||
done
|
||||
|
||||
|
|
@ -165,13 +158,10 @@ cat <<EOF
|
|||
Seeded navigator demo data via $HERDR_SOCKET_PATH
|
||||
|
||||
Workspaces:
|
||||
$WS1 nav-${stamp}-claude-review claude blocked + claude working + shell
|
||||
$WS2 nav-${stamp}-codex-build codex working x2 + idle + done, tab $T2B
|
||||
$WS3 nav-${stamp}-finished claude done x2 + codex idle
|
||||
$WS4 nav-${stamp}-quiet shell x2 + pi idle, tab $T4B
|
||||
$WS5 nav-${stamp}-mixed blocked x2 + working x2 + idle, tab $T5B
|
||||
$WS6 nav-${stamp}-dense blocked + working x4 + idle + done, tab $T6B
|
||||
$API_WS api agents: blocked codex, working claude, idle shell; logs: working deploy
|
||||
$WEB_WS web agents: working pi, done codex, blocked claude; preview: idle server
|
||||
$DOCS_WS docs release: done codex, working codex
|
||||
$INFRA_WS infra ops: blocked hermes, idle ssh
|
||||
|
||||
Sparse naming is intentional. Most panes have no manual names.
|
||||
Test in navigator: / then b, w, i, d, a, or normal text.
|
||||
EOF
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -2759,6 +2759,400 @@ td:last-child {
|
|||
}
|
||||
}
|
||||
|
||||
.content-shell {
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
|
||||
.page-hero {
|
||||
max-width: var(--max);
|
||||
margin: 0 auto;
|
||||
padding: 5rem 1.25rem 2.25rem;
|
||||
}
|
||||
|
||||
.page-hero-compact {
|
||||
max-width: 860px;
|
||||
}
|
||||
|
||||
.page-hero h1 {
|
||||
max-width: 14ch;
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-size: clamp(2.8rem, 7vw, 6rem);
|
||||
line-height: 0.92;
|
||||
letter-spacing: -0.07em;
|
||||
}
|
||||
|
||||
.article-header h1 {
|
||||
max-width: 22ch;
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-size: clamp(2rem, 4vw, 3.4rem);
|
||||
line-height: 1;
|
||||
letter-spacing: -0.055em;
|
||||
}
|
||||
|
||||
.page-hero .hero-lede {
|
||||
max-width: 680px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.section-narrow {
|
||||
max-width: 860px;
|
||||
}
|
||||
|
||||
.post-list {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.post-card {
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.post-card:last-child {
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.post-card-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
padding: 1.45rem 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.post-card h2 {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-size: clamp(1.45rem, 3vw, 2.05rem);
|
||||
line-height: 1;
|
||||
letter-spacing: -0.055em;
|
||||
transition: color 0.16s ease;
|
||||
}
|
||||
|
||||
.post-card p {
|
||||
max-width: 680px;
|
||||
margin: 0.55rem 0 0;
|
||||
color: color-mix(in srgb, var(--ink) 62%, var(--muted));
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.release-card-version {
|
||||
margin: 0 0 0.42rem !important;
|
||||
color: var(--accent) !important;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.post-card span {
|
||||
flex: 0 0 auto;
|
||||
color: var(--muted-2);
|
||||
font-family: var(--mono);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 800;
|
||||
opacity: 0.55;
|
||||
transition:
|
||||
color 0.16s ease,
|
||||
opacity 0.16s ease,
|
||||
transform 0.16s ease;
|
||||
}
|
||||
|
||||
.post-card:hover h2 {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.post-card:hover span {
|
||||
color: var(--accent);
|
||||
opacity: 1;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.text-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
color: var(--accent);
|
||||
font-weight: 750;
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 0.18em;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.55rem;
|
||||
align-items: center;
|
||||
margin: 0 0 0.85rem;
|
||||
color: color-mix(in srgb, var(--ink) 58%, var(--muted));
|
||||
font-family: var(--mono);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.post-meta > * + *::before {
|
||||
content: "/";
|
||||
margin-right: 0.55rem;
|
||||
color: var(--line-strong);
|
||||
}
|
||||
|
||||
.article-layout {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(8rem, 12rem) minmax(0, 860px);
|
||||
gap: clamp(2rem, 6vw, 4rem);
|
||||
align-items: start;
|
||||
padding-top: 4rem;
|
||||
}
|
||||
|
||||
.article-shell {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.article-toc {
|
||||
position: sticky;
|
||||
top: 6rem;
|
||||
padding-top: 0.35rem;
|
||||
color: color-mix(in srgb, var(--ink) 58%, var(--muted));
|
||||
font-family: var(--mono);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.article-toc p {
|
||||
margin: 0 0 0.7rem;
|
||||
color: color-mix(in srgb, var(--ink) 42%, var(--muted));
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.article-toc nav {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.article-toc a {
|
||||
color: color-mix(in srgb, var(--ink) 58%, var(--muted));
|
||||
line-height: 1.35;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.article-toc a:hover,
|
||||
.article-toc a[aria-current="true"] {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.article-header {
|
||||
margin-bottom: 2.35rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.article-header p:last-child {
|
||||
max-width: 680px;
|
||||
margin: 1.2rem 0 0;
|
||||
color: color-mix(in srgb, var(--ink) 68%, var(--muted));
|
||||
font-size: 1.18rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.article-prose {
|
||||
color: var(--ink);
|
||||
font-size: 1.04rem;
|
||||
line-height: 1.82;
|
||||
}
|
||||
|
||||
.article-prose > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.article-prose h1,
|
||||
.article-prose h2,
|
||||
.article-prose h3 {
|
||||
color: var(--ink);
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.article-prose h1 {
|
||||
margin: 2.3rem 0 1rem;
|
||||
font-size: clamp(2rem, 5vw, 3.5rem);
|
||||
}
|
||||
|
||||
.article-prose h2 {
|
||||
margin: 2.2rem 0 0.75rem;
|
||||
font-size: clamp(1.55rem, 3vw, 2.2rem);
|
||||
}
|
||||
|
||||
.article-prose h3 {
|
||||
margin: 1.7rem 0 0.65rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.article-prose p,
|
||||
.article-prose ul,
|
||||
.article-prose ol {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.article-prose ul,
|
||||
.article-prose ol {
|
||||
padding-left: 1.35rem;
|
||||
}
|
||||
|
||||
.article-prose li + li {
|
||||
margin-top: 0.42rem;
|
||||
}
|
||||
|
||||
.article-prose a {
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 0.18em;
|
||||
}
|
||||
|
||||
.article-prose code {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-elevated);
|
||||
color: var(--ink);
|
||||
padding: 0.08rem 0.28rem;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.article-prose pre {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-elevated);
|
||||
color: var(--ink);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.article-prose pre code {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.article-prose .expressive-code .frame {
|
||||
border-color: var(--line) !important;
|
||||
background: var(--bg-elevated) !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.article-prose .expressive-code .frame figcaption.header {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.article-prose .expressive-code pre {
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
background: var(--bg-elevated) !important;
|
||||
color: var(--ink) !important;
|
||||
}
|
||||
|
||||
.article-prose .expressive-code .code,
|
||||
.article-prose .expressive-code .code span {
|
||||
color: var(--ink) !important;
|
||||
}
|
||||
|
||||
.release-shell {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.release-header h1 {
|
||||
max-width: 820px;
|
||||
font-size: clamp(1.9rem, 3.5vw, 3.1rem);
|
||||
line-height: 1.04;
|
||||
}
|
||||
|
||||
.release-prose h2 {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.release-prose h3 {
|
||||
margin-top: 1.9rem;
|
||||
padding-top: 1.4rem;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.release-media {
|
||||
margin: 1.6rem 0 2.4rem;
|
||||
}
|
||||
|
||||
.release-media video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.release-media figcaption {
|
||||
margin-top: 0.75rem;
|
||||
color: color-mix(in srgb, var(--ink) 52%, var(--muted));
|
||||
font-family: var(--mono);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.release-video-placeholder {
|
||||
display: grid;
|
||||
min-height: 19rem;
|
||||
place-items: center;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
background:
|
||||
linear-gradient(135deg, color-mix(in srgb, var(--accent) 10%, transparent), transparent 42%),
|
||||
var(--bg-elevated);
|
||||
color: var(--ink);
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.release-video-placeholder span {
|
||||
color: var(--accent);
|
||||
font-family: var(--mono);
|
||||
font-size: 0.74rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.release-video-placeholder strong {
|
||||
display: block;
|
||||
margin-top: 0.6rem;
|
||||
font-size: clamp(1.5rem, 4vw, 2.6rem);
|
||||
line-height: 1;
|
||||
letter-spacing: -0.055em;
|
||||
}
|
||||
|
||||
.release-video-placeholder code {
|
||||
margin-top: 1rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (max-width: 1040px) {
|
||||
.article-layout {
|
||||
display: block;
|
||||
max-width: 860px;
|
||||
}
|
||||
|
||||
.article-toc {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
|
|
|
|||
|
|
@ -67,9 +67,12 @@
|
|||
<span>herdr</span>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
<a href="/">Home</a>
|
||||
<a href="/compare/">Compare</a>
|
||||
<a href="/docs/">Docs</a>
|
||||
<a href="/releases/">Releases</a>
|
||||
<a href="/blog/">Blog</a>
|
||||
<a href="/docs/quick-start/">Quick start</a>
|
||||
<a href="/docs/">Docs</a>
|
||||
<a href="/docs/socket-api/">API</a>
|
||||
</div>
|
||||
<div class="nav-tools">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,245 @@
|
|||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
canonicalPath: string;
|
||||
ogType?: 'website' | 'article';
|
||||
}
|
||||
|
||||
const { title, description, canonicalPath, ogType = 'website' } = Astro.props;
|
||||
const canonicalUrl = new URL(canonicalPath, 'https://herdr.dev').toString();
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta name="theme-color" content="#f0eee9" />
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
|
||||
<meta property="og:type" content={ogType} />
|
||||
<meta property="og:site_name" content="Herdr" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={canonicalUrl} />
|
||||
<meta property="og:image" content="https://herdr.dev/assets/og-card-v6.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:alt" content="Herdr — One terminal. The whole herd." />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content="https://herdr.dev/assets/og-card-v6.png" />
|
||||
<meta name="twitter:image:alt" content="Herdr — One terminal. The whole herd." />
|
||||
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon.png?v=14" />
|
||||
<link rel="apple-touch-icon" href="/assets/logo.png" />
|
||||
<link rel="preload" href="/assets/jbm.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<script>
|
||||
(function () {
|
||||
var saved = localStorage.getItem("herdr-palette");
|
||||
var system = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "tokyo-night"
|
||||
: "tokyo-night-day";
|
||||
document.documentElement.setAttribute("data-palette", saved || system);
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="/css/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="nav" aria-label="Primary navigation">
|
||||
<a class="brand" href="/" aria-label="Herdr home">
|
||||
<span class="brand-logo" aria-hidden="true"></span>
|
||||
<span>herdr</span>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
<a href="/">Home</a>
|
||||
<a href="/compare/">Compare</a>
|
||||
<a href="/releases/">Releases</a>
|
||||
<a href="/blog/">Blog</a>
|
||||
<a href="/docs/quick-start/">Quick start</a>
|
||||
<a href="/docs/">Docs</a>
|
||||
<a href="/docs/socket-api/">API</a>
|
||||
</div>
|
||||
<div class="nav-tools">
|
||||
<a class="nav-icon-link" href="https://github.com/ogulcancelik/herdr" aria-label="Herdr on GitHub">
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82A7.64 7.64 0 0 1 8 3.87c.68 0 1.36.09 2 .26 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="nav-icon-link" href="https://x.com/herdrdev" aria-label="Herdr on X">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231Zm-1.161 17.52h1.833L7.084 4.126H5.117Z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="nav-cta" href="/docs/install/">Install</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<slot />
|
||||
|
||||
<div class="theme-hint" id="theme-hint" title="Press / or t to switch theme">
|
||||
<span class="theme-hint-dot"></span>
|
||||
<span class="theme-hint-label" id="theme-hint-label">light</span>
|
||||
<span class="theme-hint-key">[/]</span>
|
||||
</div>
|
||||
|
||||
<div class="theme-overlay" id="theme-overlay" hidden>
|
||||
<div class="theme-overlay-panel" role="dialog" aria-label="Theme switcher" aria-modal="true">
|
||||
<div class="theme-overlay-title">settings</div>
|
||||
<div class="theme-overlay-tabs"><span class="active">theme</span></div>
|
||||
<div class="theme-overlay-list" id="theme-overlay-list" role="listbox" aria-label="Themes">
|
||||
<div class="theme-sep"><span>dark</span></div>
|
||||
<button type="button" role="option" data-value="catppuccin">catppuccin</button>
|
||||
<button type="button" role="option" data-value="terminal">terminal</button>
|
||||
<button type="button" role="option" data-value="tokyo-night">tokyo night</button>
|
||||
<button type="button" role="option" data-value="dracula">dracula</button>
|
||||
<button type="button" role="option" data-value="nord">nord</button>
|
||||
<button type="button" role="option" data-value="gruvbox">gruvbox</button>
|
||||
<button type="button" role="option" data-value="one-dark">one dark</button>
|
||||
<button type="button" role="option" data-value="solarized">solarized</button>
|
||||
<button type="button" role="option" data-value="kanagawa">kanagawa</button>
|
||||
<button type="button" role="option" data-value="rose-pine">rose pine</button>
|
||||
<button type="button" role="option" data-value="vesper">vesper</button>
|
||||
<div class="theme-sep"><span>light</span></div>
|
||||
<button type="button" role="option" data-value="catppuccin-latte">catppuccin latte</button>
|
||||
<button type="button" role="option" data-value="tokyo-night-day">tokyo day</button>
|
||||
<button type="button" role="option" data-value="gruvbox-light">gruvbox light</button>
|
||||
<button type="button" role="option" data-value="one-light">one light</button>
|
||||
<button type="button" role="option" data-value="solarized-light">solarized light</button>
|
||||
<button type="button" role="option" data-value="kanagawa-lotus">kanagawa lotus</button>
|
||||
<button type="button" role="option" data-value="rose-pine-dawn">rose pine dawn</button>
|
||||
</div>
|
||||
<div class="theme-overlay-footer">
|
||||
<span class="theme-overlay-hints"><span class="key">↑↓</span> select</span>
|
||||
<span class="theme-overlay-actions"><span class="theme-apply-btn">↵ apply</span><button class="theme-close-btn" type="button" id="theme-close-btn">esc close</button></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var overlay = document.getElementById("theme-overlay");
|
||||
var list = document.getElementById("theme-overlay-list");
|
||||
var hintLabel = document.getElementById("theme-hint-label");
|
||||
if (!overlay || !list) return;
|
||||
var options = Array.prototype.slice.call(list.querySelectorAll("[role='option']"));
|
||||
var media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
var saved = localStorage.getItem("herdr-palette") || "";
|
||||
var committed = saved || defaultPalette();
|
||||
|
||||
function defaultPalette() {
|
||||
return media.matches ? "tokyo-night" : "tokyo-night-day";
|
||||
}
|
||||
|
||||
function setPalette(value) {
|
||||
document.documentElement.setAttribute("data-palette", value || defaultPalette());
|
||||
}
|
||||
|
||||
function updateUI(value) {
|
||||
var activeValue = value || defaultPalette();
|
||||
options.forEach(function (opt) {
|
||||
opt.setAttribute("aria-selected", String(opt.dataset.value === activeValue));
|
||||
});
|
||||
var active = options.find(function (opt) { return opt.dataset.value === activeValue; });
|
||||
if (hintLabel && active) hintLabel.textContent = active.textContent.trim();
|
||||
}
|
||||
|
||||
function openOverlay() {
|
||||
overlay.hidden = false;
|
||||
document.body.style.overflow = "hidden";
|
||||
committed = localStorage.getItem("herdr-palette") || defaultPalette();
|
||||
updateUI(committed);
|
||||
var active = list.querySelector("[aria-selected='true']") || options[0];
|
||||
if (active) active.focus();
|
||||
}
|
||||
|
||||
function closeOverlay() {
|
||||
overlay.hidden = true;
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
function toggleOverlay() {
|
||||
if (overlay.hidden) openOverlay();
|
||||
else closeOverlay();
|
||||
}
|
||||
|
||||
function preview(index) {
|
||||
if (index < 0 || index >= options.length) return;
|
||||
setPalette(options[index].dataset.value);
|
||||
updateUI(options[index].dataset.value);
|
||||
options[index].focus();
|
||||
}
|
||||
|
||||
function commit(value) {
|
||||
setPalette(value);
|
||||
localStorage.setItem("herdr-palette", value);
|
||||
updateUI(value);
|
||||
closeOverlay();
|
||||
}
|
||||
|
||||
function revert() {
|
||||
setPalette(committed);
|
||||
updateUI(committed);
|
||||
closeOverlay();
|
||||
}
|
||||
|
||||
media.addEventListener("change", function () {
|
||||
if (!localStorage.getItem("herdr-palette")) {
|
||||
setPalette("");
|
||||
updateUI("");
|
||||
}
|
||||
});
|
||||
|
||||
options.forEach(function (opt, idx) {
|
||||
opt.addEventListener("click", function () { commit(opt.dataset.value); });
|
||||
opt.addEventListener("mouseenter", function () { preview(idx); });
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if ((e.key === "t" || e.key === "/") && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
var tag = document.activeElement && document.activeElement.tagName;
|
||||
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
|
||||
e.preventDefault();
|
||||
toggleOverlay();
|
||||
return;
|
||||
}
|
||||
if (overlay.hidden) return;
|
||||
var idx = options.indexOf(document.activeElement);
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
preview(idx >= 0 ? Math.min(options.length - 1, idx + 1) : 0);
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
preview(idx >= 0 ? Math.max(0, idx - 1) : options.length - 1);
|
||||
} else if (e.key === "Enter" && idx >= 0) {
|
||||
e.preventDefault();
|
||||
commit(options[idx].dataset.value);
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
revert();
|
||||
}
|
||||
});
|
||||
|
||||
var themeHint = document.getElementById("theme-hint");
|
||||
if (localStorage.getItem("herdr-theme-hint-seen") === "1") {
|
||||
themeHint.classList.add("is-seen");
|
||||
}
|
||||
themeHint.addEventListener("mouseenter", function () {
|
||||
localStorage.setItem("herdr-theme-hint-seen", "1");
|
||||
themeHint.classList.add("is-seen");
|
||||
}, { once: true });
|
||||
themeHint.addEventListener("click", toggleOverlay);
|
||||
document.getElementById("theme-close-btn").addEventListener("click", revert);
|
||||
setPalette(saved);
|
||||
updateUI(saved);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { defineCollection } from 'astro:content';
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
import { glob } from 'astro/loaders';
|
||||
import { docsLoader } from '@astrojs/starlight/loaders';
|
||||
import { docsSchema } from '@astrojs/starlight/schema';
|
||||
|
||||
|
|
@ -9,4 +10,24 @@ function docsPath({ entry }: { entry: string }) {
|
|||
|
||||
export const collections = {
|
||||
docs: defineCollection({ loader: docsLoader({ generateId: docsPath }), schema: docsSchema() }),
|
||||
blog: defineCollection({
|
||||
loader: glob({ pattern: '*.md', base: './src/content/blog' }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
date: z.coerce.date(),
|
||||
draft: z.boolean().default(false),
|
||||
}),
|
||||
}),
|
||||
releases: defineCollection({
|
||||
loader: glob({ pattern: '*.md', base: './src/content/releases' }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
version: z.string(),
|
||||
path: z.string(),
|
||||
description: z.string(),
|
||||
date: z.coerce.date(),
|
||||
draft: z.boolean().default(false),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
---
|
||||
title: Live updates without killing your terminal processes
|
||||
description: Herdr can replace its server during an update while keeping pane processes alive.
|
||||
date: 2026-05-27
|
||||
---
|
||||
|
||||
Live handoff shipped in Herdr 0.6.3 as an opt-in update path. See the [live handoff docs](/docs/session-state/#live-handoff) for the user-facing behavior.
|
||||
|
||||
Terminal multiplexers are supposed to protect long-running work from interruption. You start a server, run shells and tools inside panes, detach clients, reconnect later, and the work keeps going.
|
||||
|
||||
Updating the multiplexer itself usually breaks that.
|
||||
|
||||
That is normal for tools like tmux and Zellij. They are mature, stable, and their core job changes slowly. If the server/client protocol changes, the usual answer is to keep using the old server until you can restart it. That is annoying, but often acceptable.
|
||||
|
||||
Herdr has a different problem. It is a terminal multiplexer, sure, but it is also an AI development workspace. It is new. I released it a few months ago, and we are at 2.6k stars on GitHub. It has more than 15k downloads according to GitHub release stats.
|
||||
|
||||
It gets frequent fixes for agent detection (isn't it cool that we have all the labs building their own CLIs, plugins, and hooks in their own broken way?), terminal behavior, remote workflows, worktrees, mobile use, update flows, and integrations. Users should not have to choose between getting those fixes and killing every pane in a running session.
|
||||
|
||||
That's a problem: Herdr needed to update like an app while preserving processes like a terminal multiplexer.
|
||||
|
||||
## The update problem
|
||||
|
||||
A persistent terminal multiplexer usually has at least two sides.
|
||||
|
||||
The server owns the session. It owns pane state, PTYs, readers, writers, layout, buffers, sockets, and child process lifetime. The client is mostly a view and input path into that server.
|
||||
|
||||
When a new binary changes the client/server protocol, the new client may not be able to talk to the old server. Restarting the server fixes that, but it also risks killing the pane processes. For a terminal multiplexer, that is the worst possible update path.
|
||||
|
||||
This is not only a Herdr problem. I looked around because I assumed somebody had already solved it.
|
||||
|
||||
Zellij has multiple long-running issues about upgrade pain: sessions appearing lost after upgrade, users being unable to attach to sessions from a previous version, requests for an `upgrade-session` flow, and requests to import sessions from previous versions. See [zellij#1255](https://github.com/zellij-org/zellij/issues/1255), [zellij#2943](https://github.com/zellij-org/zellij/issues/2943), [zellij#3371](https://github.com/zellij-org/zellij/issues/3371), [zellij#3420](https://github.com/zellij-org/zellij/issues/3420), [zellij#3449](https://github.com/zellij-org/zellij/issues/3449), and [zellij#4305](https://github.com/zellij-org/zellij/issues/4305). Zellij 0.44 added forward-compatible client/server support, which helps future clients connect to older running sessions, but that is still not replacing the running server while preserving live PTYs.
|
||||
|
||||
Tmux has the same class of version-skew behavior. Users hit protocol mismatch errors, and the practical answer is to restart tmux, delay the update, or attach with the old binary through `/proc/<pid>/exe` where that exists. See [tmux#99](https://github.com/tmux/tmux/issues/99), [tmux#2189](https://github.com/tmux/tmux/issues/2189), [tmux#4356](https://github.com/tmux/tmux/issues/4356), and [tmux#4890](https://github.com/tmux/tmux/issues/4890).
|
||||
|
||||
As far as I can tell, mainstream terminal multiplexers do not do in-place server binary replacement while keeping arbitrary live pane processes attached.
|
||||
|
||||
So, that is a small breakthrough, but a real one.
|
||||
|
||||
## The important detail is PTY ownership
|
||||
|
||||
At first, the problem sounds like process migration. But no, that would be much harder.
|
||||
|
||||
Herdr does not move the child processes. It moves ownership of the terminals those processes are already attached to.
|
||||
|
||||
Each pane process talks to the slave side of a PTY. Herdr's server owns the master side. If the final master FD disappears, the child can lose I/O, receive HUP, or exit. But if the master FD stays alive and a new server takes over reading and writing it, the child process does not need to know the server changed.
|
||||
|
||||
That changes the problem from “move a process” to “transfer open file descriptors and rebuild runtime ownership around them.”
|
||||
|
||||
On Unix, that is possible with `SCM_RIGHTS`: one process can send open file descriptors to another process over a Unix socket.
|
||||
|
||||
## The handoff
|
||||
|
||||
The final shape is a two-server transaction.
|
||||
|
||||
```text
|
||||
old server: pause PTY readers
|
||||
old server: snapshot pure app state
|
||||
old server: duplicate PTY master FDs
|
||||
old server: spawn new Herdr in import mode
|
||||
handoff: send manifest + FDs over a private Unix socket
|
||||
new server: restore state and rebuild pane runtimes
|
||||
new server: bind public sockets and report ready
|
||||
old server: commit and exit without killing pane process groups
|
||||
new server: unpause readers and continue
|
||||
```
|
||||
|
||||
The old server does not just dump state and hope. It enters handoff mode, rejects new mutations, disconnects clients, and pauses every transferred PTY reader. That pause acknowledgement matters because only one server may actively read from a PTY at a time. If both servers read from the same master, output can be stolen by the wrong runtime.
|
||||
|
||||
After readers are paused, the old server captures Herdr's normal session snapshot. That works because Herdr already separates pure application state from runtime ownership. `AppState` can be serialized. Pane runtimes, PTY handles, readers, writers, and detector loops are separate runtime objects.
|
||||
|
||||
Then the old server creates a handoff manifest and sends the duplicated PTY master FDs to the new server.
|
||||
|
||||
```text
|
||||
manifest = app snapshot + pane metadata + optional replay bytes
|
||||
FDs = duplicated PTY master file descriptors
|
||||
transport = private Unix socket + SCM_RIGHTS
|
||||
```
|
||||
|
||||
The new server validates the manifest, checks the expected protocol and version, receives the FDs, restores `AppState`, constructs imported pane runtimes around the received PTY masters, binds the normal Herdr sockets, and reports ready.
|
||||
|
||||
Only then does the old server commit. Before commit, any failure rolls back to the old server. After commit, the new server owns the panes and the old server exits without signaling the pane process groups.
|
||||
|
||||
That commit boundary is the safety line.
|
||||
|
||||
## Why Herdr could do it
|
||||
|
||||
Herdr had the pieces already, mostly because of earlier architecture choices.
|
||||
|
||||
The state model is separated from runtime ownership. Workspaces, tabs, panes, labels, focus, layout, and agent metadata live in serializable state. PTYs, child handles, reader threads, writer paths, resize state, and detectors live in runtime objects.
|
||||
|
||||
The server/client boundary is explicit. Herdr already has a persistent server, reconnecting clients, local sockets, named sessions, remote attach flows, and documented [session state paths](/docs/session-state/). That made it possible to add a hidden import mode instead of rewriting the app around live upgrades.
|
||||
|
||||
The terminal engine also helped with visual continuity. Herdr uses `libghostty-vt` for pane terminal state. For normal primary-screen panes, the old server can export recent ANSI history and the new server can seed that history before it starts reading future PTY bytes. That is not required for process survival, but it makes shells and dev servers look continuous after reconnect.
|
||||
|
||||
Full-screen terminal apps are different. Herdr does not fake replay for alternate-screen panes like `nvim`, `btop`, or `htop`. Their processes survive the handoff, but the visual state is best effort until the app redraws. That is the honest trade-off.
|
||||
|
||||
## The invariants
|
||||
|
||||
The hard part was not sending FDs. The hard part was making the transaction safe.
|
||||
|
||||
These were the rules:
|
||||
|
||||
- exactly one server may read each PTY at a time
|
||||
- every transferred reader must acknowledge pause before snapshot and replay capture
|
||||
- before commit, failure leaves the old server alive and owning the panes
|
||||
- after commit, the old server must not destructively drop transferred runtimes
|
||||
- no failure path may close the final PTY master FD
|
||||
- agent restore must not run during [live handoff](/docs/session-state/#live-handoff)
|
||||
- alternate-screen panes must stay live without pretending their screen was perfectly serialized
|
||||
|
||||
The implementation was tested with shell panes, a Python HTTP server, `btop`, and `nvim` on Linux and macOS. The HTTP server kept serving requests across handoff. Shell panes accepted input after handoff. Full-screen TUIs stayed alive and redrew on a best-effort basis.
|
||||
|
||||
## What this is not
|
||||
|
||||
This is not checkpoint/restore. It does not survive a reboot. It does not migrate processes across machines. It does not preserve arbitrary process memory. It does not make every future internal state change automatically compatible.
|
||||
|
||||
It is a same-machine Unix handoff. The running pane process stays attached to its PTY slave. The old Herdr server transfers the PTY master to the new Herdr server. The server binary changes; the pane process keeps running.
|
||||
|
||||
That is the useful boundary.
|
||||
|
||||
## Why this is cool
|
||||
|
||||
Herdr users often keep the exact kind of work open that should not be casually restarted: coding agents, dev servers, shells, REPLs, SSH sessions, debugging sessions, long builds, and local experiments.
|
||||
|
||||
A terminal multiplexer should make that work safer, not make updates feel dangerous.
|
||||
|
||||
The interesting part is not that Herdr remembers your layout. The interesting part is that the process inside the pane does not have to know an update happened.
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
---
|
||||
title: "session persistence, safer updates, and workspace navigation"
|
||||
version: "0.6.3"
|
||||
path: "0.6.3/session-persistence-safer-updates-and-workspace-navigation"
|
||||
description: "This one adds new persistence paths for updates, server restarts, and larger sessions."
|
||||
date: 2026-05-27
|
||||
---
|
||||
|
||||
## Persistence for the hard parts
|
||||
|
||||
Session persistence is not one feature. A detached client, a stopped server, a terminal history replay, an agent conversation restore, and an update handoff all protect different parts of the workspace.
|
||||
|
||||
Herdr 0.6.3 pushes on three of those boundaries.
|
||||
|
||||
### Agent restore after server restart
|
||||
|
||||
When a Herdr server stops, normal pane processes stop with it. Native agent restore is the path for getting supported agent conversations back after that kind of restart.
|
||||
|
||||
With `[session] resume_agents_on_restore = true`, Herdr can use official session references from supported integrations to relaunch Pi, Claude Code, Codex, OpenCode, and Hermes panes into their previous conversations.
|
||||
|
||||
[Read the session restore docs](/docs/session-state/#native-agent-session-restore).
|
||||
|
||||
### Pane screen history
|
||||
|
||||
Pane screen history solves the visual half of restart recovery. When enabled, Herdr saves recent pane output and replays it into restored panes so the workspace does not come back as a set of empty terminals.
|
||||
|
||||
It is useful for shells, logs, dev servers, and agent panes where recent output explains what was happening before the server stopped.
|
||||
|
||||
[Read how pane history differs from live persistence](/docs/session-state/#what-survives).
|
||||
|
||||
### Live handoff for updates
|
||||
|
||||
Live handoff is the update path for supported running sessions. Instead of stopping the old server and killing pane processes, Herdr can ask the old server to transfer live PTY ownership to the updated server.
|
||||
|
||||
It is currently opt-in with `herdr update --handoff` and `herdr --remote <host> --handoff`. That keeps the default path conservative while the feature gets more real-world coverage.
|
||||
|
||||
[Read the live handoff docs](/docs/session-state/#live-handoff).
|
||||
|
||||
For the technical background, read the deep dive: [Live updates without killing your terminal processes](/blog/live-updates-without-killing-your-terminal-processes/).
|
||||
|
||||
## Session navigator
|
||||
|
||||
Large Herdr sessions need fast movement. The new session navigator opens with `prefix+g` and gives you a searchable workspace, tab, and pane tree.
|
||||
|
||||
At a glance, you can see the full tree of workspaces, tabs, and panes. Press `/` to fuzzy find by text. Press `b`, `w`, `i`, or `d` to filter by blocked, working, idle, or done panes. Press `a` or Backspace to clear the state filter.
|
||||
|
||||
Use it to jump straight to the pane that needs attention without manually scanning the sidebar.
|
||||
|
||||
<figure class="release-media">
|
||||
<video src="/assets/releases/v0.6.3/session-navigator.mp4" controls playsinline preload="metadata"></video>
|
||||
<figcaption>
|
||||
The session navigator shows workspaces, tabs, panes, and agent states in one searchable tree.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
## More terminal workflow polish
|
||||
|
||||
Direct agent terminal attaches now have scrollback, so PageUp, PageDown, and mouse wheel scrolling work in the attach viewport when the running terminal app has not requested that input for itself.
|
||||
|
||||
Herdr also gained better update and remote-bootstrap behavior for package-manager installs, more reliable event delivery under load, and fixes across agent detection, terminal colors, sound alerts, and integration lifecycle handling.
|
||||
|
||||
## Pane metadata reporting
|
||||
|
||||
The new `pane.report_metadata` socket method and `herdr pane report-metadata` CLI command let integrations and local hooks customize what Herdr shows for a pane.
|
||||
|
||||
After installing an integration, the integration can report a title, visible agent name, compact status label, state label, and native session metadata without taking over unrelated lifecycle behavior.
|
||||
|
||||
You can also use it from your own scripts. For example, a hook can mark a pane as running a deploy, rename a long-lived task, or ask an agent to report a more specific state for the work it is doing.
|
||||
|
||||
```bash
|
||||
herdr pane report-metadata \
|
||||
--title "api deploy" \
|
||||
--agent-name "codex" \
|
||||
--state-label "reviewing logs" \
|
||||
--compact-status "deploy"
|
||||
```
|
||||
|
||||
This gives integrations a supported way to make panes more readable while keeping Herdr's session and agent ownership rules intact.
|
||||
|
||||
[Read the socket API docs](/docs/socket-api/).
|
||||
|
||||
## Full notes
|
||||
|
||||
This page highlights the product changes. The complete release notes remain on GitHub.
|
||||
|
||||
[Open the 0.6.3 GitHub release](https://github.com/ogulcancelik/herdr/releases/tag/v0.6.3).
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import MarketingLayout from '../../components/MarketingLayout.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog');
|
||||
return posts
|
||||
.filter((post) => !post.data.draft)
|
||||
.map((post) => {
|
||||
const slug = post.id.replace(/\.(md|mdx)$/i, '');
|
||||
return {
|
||||
params: { slug },
|
||||
props: { post, slug },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const { post, slug } = Astro.props;
|
||||
const { Content, headings } = await render(post);
|
||||
const sectionHeadings = headings.filter((heading) => heading.depth === 2);
|
||||
---
|
||||
|
||||
<MarketingLayout
|
||||
title={`${post.data.title} — Herdr blog`}
|
||||
description={post.data.description}
|
||||
canonicalPath={`/blog/${slug}/`}
|
||||
ogType="article"
|
||||
>
|
||||
<main class="content-shell">
|
||||
<div class="article-layout section">
|
||||
<aside class="article-toc" aria-label="Article sections">
|
||||
<p>Sections</p>
|
||||
<nav>
|
||||
{sectionHeadings.map((heading) => (
|
||||
<a href={`#${heading.slug}`} data-heading-link={heading.slug}>{heading.text}</a>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<article class="article-shell">
|
||||
<header class="article-header">
|
||||
<p class="post-meta">
|
||||
<a href="/blog/">Blog</a>
|
||||
<time datetime={post.data.date.toISOString()}>{post.data.date.toLocaleDateString('en', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}</time>
|
||||
</p>
|
||||
<h1>{post.data.title}</h1>
|
||||
<p>{post.data.description}</p>
|
||||
</header>
|
||||
<div class="article-prose">
|
||||
<Content />
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const headings = Array.from(document.querySelectorAll('.article-prose h2[id]'));
|
||||
const links = new Map(
|
||||
Array.from(document.querySelectorAll('[data-heading-link]')).map((link) => [
|
||||
link.getAttribute('data-heading-link'),
|
||||
link,
|
||||
]),
|
||||
);
|
||||
|
||||
function setActive(id) {
|
||||
for (const link of links.values()) link.removeAttribute('aria-current');
|
||||
links.get(id)?.setAttribute('aria-current', 'true');
|
||||
}
|
||||
|
||||
if (headings.length > 0 && links.size > 0) {
|
||||
setActive(headings[0].id);
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries
|
||||
.filter((entry) => entry.isIntersecting)
|
||||
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)[0];
|
||||
if (visible?.target instanceof HTMLElement) setActive(visible.target.id);
|
||||
},
|
||||
{ rootMargin: '-18% 0px -68% 0px', threshold: 0 },
|
||||
);
|
||||
for (const heading of headings) observer.observe(heading);
|
||||
}
|
||||
</script>
|
||||
</MarketingLayout>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import MarketingLayout from '../../components/MarketingLayout.astro';
|
||||
|
||||
const posts = (await getCollection('blog'))
|
||||
.filter((post) => !post.data.draft)
|
||||
.map((post) => ({ ...post, slug: post.id.replace(/\.(md|mdx)$/i, '') }))
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||
---
|
||||
|
||||
<MarketingLayout
|
||||
title="Herdr blog"
|
||||
description="Notes on terminal-native agent workflows and the engineering behind Herdr."
|
||||
canonicalPath="/blog/"
|
||||
>
|
||||
<main class="content-shell">
|
||||
<section class="page-hero page-hero-compact">
|
||||
<p class="eyebrow"><span class="status-dot dot-working"></span>Blog</p>
|
||||
<h1>Notes from Herdr.</h1>
|
||||
<p class="hero-lede">
|
||||
Product notes and implementation writeups for the terminal-native agent multiplexer.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="section section-narrow" aria-label="Blog posts">
|
||||
<div class="post-list">
|
||||
{posts.map((post) => (
|
||||
<article class="post-card">
|
||||
<a class="post-card-link" href={`/blog/${post.slug}/`}>
|
||||
<h2>{post.data.title}</h2>
|
||||
<span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</MarketingLayout>
|
||||
|
|
@ -55,9 +55,12 @@
|
|||
<span>herdr</span>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
<a href="/">Home</a>
|
||||
<a href="/compare/" aria-current="page">Compare</a>
|
||||
<a href="/docs/">Docs</a>
|
||||
<a href="/releases/">Releases</a>
|
||||
<a href="/blog/">Blog</a>
|
||||
<a href="/docs/quick-start/">Quick start</a>
|
||||
<a href="/docs/">Docs</a>
|
||||
<a href="/docs/socket-api/">API</a>
|
||||
</div>
|
||||
<div class="nav-tools">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import MarketingLayout from '../../components/MarketingLayout.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const releases = await getCollection('releases');
|
||||
return releases
|
||||
.filter((release) => !release.data.draft)
|
||||
.map((release) => {
|
||||
const slug = release.data.path;
|
||||
return {
|
||||
params: { slug },
|
||||
props: { release, slug },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const { release, slug } = Astro.props;
|
||||
const { Content, headings } = await render(release);
|
||||
const sectionHeadings = headings.filter((heading) => heading.depth === 2);
|
||||
---
|
||||
|
||||
<MarketingLayout
|
||||
title={`${release.data.version}: ${release.data.title} — Herdr releases`}
|
||||
description={release.data.description}
|
||||
canonicalPath={`/releases/${slug}/`}
|
||||
ogType="article"
|
||||
>
|
||||
<main class="content-shell">
|
||||
<div class="article-layout release-layout section">
|
||||
<aside class="article-toc" aria-label="Release sections">
|
||||
<p>Sections</p>
|
||||
<nav>
|
||||
{sectionHeadings.map((heading) => (
|
||||
<a href={`#${heading.slug}`} data-heading-link={heading.slug}>{heading.text}</a>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<article class="article-shell release-shell">
|
||||
<header class="article-header release-header">
|
||||
<p class="post-meta">
|
||||
<a href="/releases/">Releases</a>
|
||||
<span>{release.data.version}</span>
|
||||
<time datetime={release.data.date.toISOString()}>{release.data.date.toLocaleDateString('en', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}</time>
|
||||
</p>
|
||||
<h1>{release.data.title}</h1>
|
||||
<p>{release.data.description}</p>
|
||||
</header>
|
||||
<div class="article-prose release-prose">
|
||||
<Content />
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const headings = Array.from(document.querySelectorAll('.article-prose h2[id]'));
|
||||
const links = new Map(
|
||||
Array.from(document.querySelectorAll('[data-heading-link]')).map((link) => [
|
||||
link.getAttribute('data-heading-link'),
|
||||
link,
|
||||
]),
|
||||
);
|
||||
|
||||
function setActive(id) {
|
||||
for (const link of links.values()) link.removeAttribute('aria-current');
|
||||
links.get(id)?.setAttribute('aria-current', 'true');
|
||||
}
|
||||
|
||||
if (headings.length > 0 && links.size > 0) {
|
||||
setActive(headings[0].id);
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries
|
||||
.filter((entry) => entry.isIntersecting)
|
||||
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)[0];
|
||||
if (visible?.target instanceof HTMLElement) setActive(visible.target.id);
|
||||
},
|
||||
{ rootMargin: '-18% 0px -68% 0px', threshold: 0 },
|
||||
);
|
||||
for (const heading of headings) observer.observe(heading);
|
||||
}
|
||||
</script>
|
||||
</MarketingLayout>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import MarketingLayout from '../../components/MarketingLayout.astro';
|
||||
|
||||
const releases = (await getCollection('releases'))
|
||||
.filter((release) => !release.data.draft)
|
||||
.map((release) => ({ ...release, slug: release.data.path }))
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||
---
|
||||
|
||||
<MarketingLayout
|
||||
title="Herdr releases"
|
||||
description="Product-focused release notes for Herdr."
|
||||
canonicalPath="/releases/"
|
||||
>
|
||||
<main class="content-shell">
|
||||
<section class="page-hero page-hero-compact">
|
||||
<p class="eyebrow"><span class="status-dot dot-working"></span>Releases</p>
|
||||
<h1>What changed in Herdr.</h1>
|
||||
<p class="hero-lede">
|
||||
See what the herd has been working on.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="section section-narrow" aria-label="Releases">
|
||||
<div class="post-list">
|
||||
{releases.map((release) => (
|
||||
<article class="post-card">
|
||||
<a class="post-card-link" href={`/releases/${release.slug}/`}>
|
||||
<div>
|
||||
<p class="release-card-version">{release.data.version}</p>
|
||||
<h2>{release.data.title}</h2>
|
||||
<p>{release.data.description}</p>
|
||||
</div>
|
||||
<span aria-hidden="true">-></span>
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</MarketingLayout>
|
||||
Loading…
Reference in New Issue