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
33 changes: 24 additions & 9 deletions internal/docscheck/docs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func TestPublicDocumentationContractsStayAligned(t *testing.T) {
"patterns/shared-web-roost/INSTRUCTIONS.md": {"WT_ROOST_ALLOWED_EMAILS", "OAuth by itself proves"},
"patterns/hosted-browser-wing/INSTRUCTIONS.md": {"Account access and wing policy are independent", "wt login", "wt start", "self-hosted roost", "application-encrypted"},
"internal/relay/templates/base.html": {`href="/install" class="nav-cta">install locally`, `href="/login">login`, ">open app</a>"},
"internal/relay/templates/home.html": {"local-first control for coding agents", "wt mcp stdio", "wt egg", "wt mcp connect", "wt serve --local --https", "optional hosted browser"},
"internal/relay/templates/home.html": {"local-first control for coding agents", "Give an agent typed control of local coding agents", `href="/patterns">choose a route</a>`},
"internal/relay/templates/docs.html": {"WT_BASE_URL=https://roost.example.com", "WT_ROOST_ALLOWED_EMAILS", "wt roost start --addr :8080", "wt serve --addr :8080", "never silently changes", "gateway database contains", "does not let an account grant itself relay access", "no request happens until you choose load or open", "200,000 serialized terminal characters", "a wing binary from before", "stop or isolate the wing", "Remote execution through direct MCP or the hosted browser needs a running wing", "do not require a separate wing daemon", "connector token", "wt login", "wt start"},
"internal/relay/templates/patterns.html": {"/patterns/hosted-browser-wing/INSTRUCTIONS.md", "account with hosted-relay access", "hosted browser -> encrypted relay -> selected wing", "authorization for the connector account", "parent/connector needs"},
"internal/relay/templates/privacy.html": {"gateway database contains", "embedded wing separately keeps", "200,000 serialized terminal characters", "Clearing the site's browser data removes"},
Expand Down Expand Up @@ -178,6 +178,29 @@ func TestPublicDocumentationContractsStayAligned(t *testing.T) {
}
}

func TestPublicHomeStaysConciseAndVideoFirst(t *testing.T) {
root := repositoryRoot(t)
data, err := os.ReadFile(filepath.Join(root, "internal/relay/templates/home.html"))
if err != nil {
t.Fatal(err)
}
home := string(data)

const intro = `<p class="tagline">Give an agent typed control of local coding agents, using the code and provider login already on this machine.</p>`
if got := strings.Count(home, intro); got != 1 {
t.Fatalf("homepage contains %d concise agent-first intros, want 1", got)
}
if !strings.Contains(home, intro+"\n"+`{{if .HeroVideo}}<div class="hero-video"`) {
t.Fatal("homepage hero video does not immediately follow the concise agent-first intro")
}
if !strings.Contains(home, `{{end}}`+"\n"+`{{if .User}}<div class="prompt-flow">`) {
t.Fatal("homepage install/app prompt does not immediately follow the hero video")
}
if strings.Contains(home, `data-route=`) {
t.Fatal("homepage must not contain the detailed data-route map")
}
}

func TestPublicEntryPointsKeepLocalAgentFirstHierarchy(t *testing.T) {
root := repositoryRoot(t)
read := func(rel string) string {
Expand Down Expand Up @@ -213,7 +236,6 @@ func TestPublicEntryPointsKeepLocalAgentFirstHierarchy(t *testing.T) {
},
}
for _, rel := range []string{
"internal/relay/templates/home.html",
"internal/relay/templates/docs.html",
"internal/relay/templates/install.html",
"internal/relay/templates/patterns.html",
Expand Down Expand Up @@ -251,13 +273,6 @@ func TestPublicEntryPointsKeepLocalAgentFirstHierarchy(t *testing.T) {
"run the browser portal locally first",
"already has hosted-relay access",
},
"internal/relay/templates/home.html": {
"<code>wt mcp stdio</code> · no account or daemon",
"<code>wt egg</code> · sandboxed and attachable",
"direct remote MCP to an explicit wing",
"<code>wt serve --local --https</code> · self-host first",
"requires hosted-relay entitlement and an allowing wing",
},
"internal/relay/templates/docs.html": {
"existing project directories and the current OS user's existing provider logins",
"connector token",
Expand Down
44 changes: 18 additions & 26 deletions internal/relay/relay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,8 +496,9 @@ func TestSignedInHomeUsesConfiguredAppURLForLinkAndShortcut(t *testing.T) {
}
}

func TestPublicHomeLeadsWithLocalAgentFirstRoutes(t *testing.T) {
_, ts := testServer(t)
func TestPublicHomeLeadsWithConciseLocalAgentIntro(t *testing.T) {
server, ts := testServer(t)
server.Config.HeroVideo = "hero.mp4"
resp, err := http.Get(ts.URL + "/")
if err != nil {
t.Fatalf("GET /: %v", err)
Expand All @@ -508,32 +509,11 @@ func TestPublicHomeLeadsWithLocalAgentFirstRoutes(t *testing.T) {
t.Fatalf("read /: %v", err)
}
page := body.String()
previous := -1
for _, route := range []string{
`href="/patterns/local-subagents/INSTRUCTIONS.md" data-route="local-agent"`,
`href="/patterns/local-sandbox/INSTRUCTIONS.md" data-route="local-human"`,
`href="/patterns/remote-orchestration/INSTRUCTIONS.md" data-route="direct-remote"`,
`href="/patterns/personal-remote-wing/INSTRUCTIONS.md" data-route="self-hosted-browser"`,
`href="/patterns/hosted-browser-wing/INSTRUCTIONS.md" data-route="hosted-relay"`,
} {
index := strings.Index(page, route)
if index < 0 {
t.Fatalf("home is missing route %q", route)
}
if index <= previous {
t.Fatalf("home route %q is out of local-agent-first order", route)
}
previous = index
if strings.Contains(page, `data-route=`) {
t.Fatal("home includes detailed route-map links")
}
for _, contract := range []string{
"wt mcp stdio",
"no account or daemon",
"wt egg",
"wt mcp connect",
"direct remote MCP to an explicit wing",
"wt serve --local --https",
"self-host first",
"requires hosted-relay entitlement and an allowing wing",
`href="/patterns">choose a route</a>`,
`href="/install" class="nav-cta">install locally`,
`href="/login">login`,
`href="/install" class="prompt-line" id="prompt-app"`,
Expand All @@ -542,6 +522,18 @@ func TestPublicHomeLeadsWithLocalAgentFirstRoutes(t *testing.T) {
t.Errorf("home does not contain hierarchy contract %q", contract)
}
}
const intro = `<p class="tagline">Give an agent typed control of local coding agents, using the code and provider login already on this machine.</p>`
if got := strings.Count(page, intro); got != 1 {
t.Fatalf("home contains %d concise agent-first intros, want 1", got)
}
if !strings.Contains(page, intro+"\n"+`<div class="hero-video"`) {
t.Fatal("home video does not immediately follow the agent-first intro")
}
video := strings.Index(page, `<div class="hero-video"`)
install := strings.Index(page, `<div class="prompt-flow" id="prompt-flow">`)
if video < 0 || install <= video {
t.Fatalf("home hero order video=%d install=%d, want video then install", video, install)
}
for _, forbidden := range []string{">hosted app</a>", ">hosted login</a>"} {
if strings.Contains(page, forbidden) {
t.Errorf("home nav uses deployment-specific label %q", forbidden)
Expand Down
20 changes: 2 additions & 18 deletions internal/relay/templates/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,6 @@
.hero{padding:40px 0 24px;text-align:center}
.hero h1{font-size:28px;font-weight:700;letter-spacing:-0.03em;margin-bottom:8px}
.hero .tagline{color:var(--text-dim);font-size:15px;line-height:1.6;max-width:720px;margin:0 auto 24px}
.route-map{max-width:900px;margin:0 auto 22px;text-align:left;display:grid;gap:8px}
.route-map a{display:grid;grid-template-columns:34px 220px 1fr;gap:12px;align-items:baseline;background:var(--bg-card);border:1px solid var(--accent);border-radius:6px;padding:10px 14px;text-decoration:none;color:var(--text)}
.route-map a:hover{border-color:var(--action)}
.route-num{font-family:var(--mono);font-size:11px;color:var(--action)}
.route-title{font-weight:600;font-size:13px}
.route-detail{font-size:12px;color:var(--text-dim)}
.route-detail code{color:var(--text)}
.prompt-flow{font-family:var(--mono);font-size:18px;margin-top:8px}
.prompt-line{display:flex;align-items:center;justify-content:center;gap:6px;padding:8px 0}
.prompt-dim{color:var(--text-dim)}
Expand Down Expand Up @@ -81,8 +74,6 @@
.sb-tip{font-family:var(--mono);font-size:11px;color:var(--text-dim);line-height:1.6;margin-top:12px;padding:0 4px}
@media(max-width:700px){
.hero h1{font-size:22px}
.route-map a{grid-template-columns:28px 1fr;gap:4px 8px}
.route-detail{grid-column:2}
.how-grid{grid-template-columns:1fr}
.sb-layout{grid-template-columns:1fr}
.install{font-size:11px;padding:12px 14px}
Expand All @@ -108,21 +99,14 @@
</script>
<div class="hero">
<h1>local-first control for coding agents</h1>
<p class="tagline">Give an agent typed control of local agents on the code and provider login already on this machine. Move to direct remote MCP only when execution moves to another machine, and add a browser only when a person needs one.</p>
<div class="route-map" aria-label="Wingthing setup routes">
<a href="/patterns/local-subagents/INSTRUCTIONS.md" data-route="local-agent"><span class="route-num">01</span><span class="route-title">agent controls local agents</span><span class="route-detail"><code>wt mcp stdio</code> · no account or daemon</span></a>
<a href="/patterns/local-sandbox/INSTRUCTIONS.md" data-route="local-human"><span class="route-num">02</span><span class="route-title">person starts a local agent terminal</span><span class="route-detail"><code>wt egg</code> · sandboxed and attachable</span></a>
<a href="/patterns/remote-orchestration/INSTRUCTIONS.md" data-route="direct-remote"><span class="route-num">03</span><span class="route-title">machines differ</span><span class="route-detail"><code>wt mcp connect</code> · direct remote MCP to an explicit wing</span></a>
<a href="/patterns/personal-remote-wing/INSTRUCTIONS.md" data-route="self-hosted-browser"><span class="route-num">04</span><span class="route-title">person needs a browser</span><span class="route-detail"><code>wt serve --local --https</code> · self-host first</span></a>
<a href="/patterns/hosted-browser-wing/INSTRUCTIONS.md" data-route="hosted-relay"><span class="route-num">05</span><span class="route-title">optional hosted browser</span><span class="route-detail">requires hosted-relay entitlement and an allowing wing</span></a>
</div>
<p class="tagline">Give an agent typed control of local coding agents, using the code and provider login already on this machine.</p>
{{if .HeroVideo}}<div class="hero-video" onclick="(function(){var v=document.getElementById('hero-vid');if(v.requestFullscreen)v.requestFullscreen();else if(v.webkitRequestFullscreen)v.webkitRequestFullscreen()})()"><video id="hero-vid" src="/hero.mp4" autoplay muted loop playsinline onloadedmetadata="this.playbackRate=0.8"></video><button class="hero-fs"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3"/></svg></button></div>{{end}}
{{if .User}}<div class="prompt-flow">
<a href="{{.AppURL}}" class="prompt-line" id="prompt-app"><span class="prompt-dim">press</span> <span class="prompt-key">.</span> <span class="prompt-dim">to</span> <span class="prompt-key">open optional hosted app</span></a>
</div>
{{else}}<div class="prompt-flow" id="prompt-flow">
<a href="/install" class="prompt-line" id="prompt-app"><span class="prompt-dim">press</span> <span class="prompt-key">.</span> <span class="prompt-dim">to</span> <span class="prompt-key">install locally</span></a>
</div>{{end}}
{{if .HeroVideo}}<div class="hero-video" onclick="(function(){var v=document.getElementById('hero-vid');if(v.requestFullscreen)v.requestFullscreen();else if(v.webkitRequestFullscreen)v.webkitRequestFullscreen()})()"><video id="hero-vid" src="/hero.mp4" autoplay muted loop playsinline onloadedmetadata="this.playbackRate=0.8"></video><button class="hero-fs"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3"/></svg></button></div>{{end}}
</div>
<div class="tab-bar">
<button class="active" onclick="switchTab('how')">how it works</button>
Expand Down
60 changes: 48 additions & 12 deletions test/web/direct-only.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,54 @@ try {
const publicPage = await publicContext.newPage();
await publicPage.goto(BASE + '/', { waitUntil: 'domcontentloaded' });
const publicState = await publicPage.evaluate(() => {
const text = (element) => element ? element.textContent.trim().replace(/\s+/g, ' ') : '';
const isVisible = (element) => {
if (!element) return false;
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
};
const comesBefore = (first, second) => Boolean(first && second &&
(first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING));
const nav = document.querySelector('nav.site-nav');
const logo = nav.querySelector('.logo').getBoundingClientRect();
const links = nav.querySelector('.nav-links');
const linkElements = Array.from(links.querySelectorAll('a'));
const linkRects = linkElements.map((link) => link.getBoundingClientRect());
const routeElements = Array.from(document.querySelectorAll('.route-map [data-route]'));
const selfHosted = routeElements.find((route) => route.dataset.route === 'self-hosted-browser');
const hero = document.querySelector('.container > .hero');
const heroLinks = hero ? Array.from(hero.querySelectorAll('a[href]')) : [];
const heroVideo = hero ? hero.querySelector('video, iframe[title*="video" i], [data-hero-video]') : null;
const installCTA = heroLinks.find((link) => /\binstall\b/i.test(text(link)));
const patternsLink = Array.from(document.querySelectorAll('a[href]')).find((link) => {
const url = new URL(link.getAttribute('href'), location.href);
return url.origin === location.origin && url.pathname.replace(/\/$/, '') === '/patterns' &&
text(link).toLowerCase() === 'patterns';
});
const introElements = hero ? Array.from(hero.children)
.filter((element) => element.matches('h1, p.tagline')) : [];
const introEnd = introElements[introElements.length - 1];
const introText = introElements.map(text).join(' ');
return {
navLabels: linkElements.map((link) => link.textContent.trim()),
ctaLabels: linkElements.filter((link) => link.classList.contains('nav-cta')).map((link) => link.textContent.trim()),
flexWrap: getComputedStyle(links).flexWrap,
linksBelowLogo: links.getBoundingClientRect().top >= logo.bottom,
linksInsideViewport: linkRects.every((rect) => rect.width > 0 && rect.left >= 0 && rect.right <= innerWidth + 0.5),
noHorizontalOverflow: document.documentElement.scrollWidth <= innerWidth,
routes: routeElements.map((route) => route.dataset.route),
selfHostedHref: selfHosted ? selfHosted.getAttribute('href') : '',
selfHostedText: selfHosted ? selfHosted.textContent : '',
headingCount: introElements.filter((element) => element.matches('h1')).length,
introText,
routeMapCount: document.querySelectorAll('.route-map').length,
dataRouteCount: document.querySelectorAll('[data-route]').length,
patternsHref: patternsLink ? new URL(patternsLink.getAttribute('href'), location.href).pathname : '',
patternsVisible: isVisible(patternsLink),
heroConfigured: Boolean(heroVideo),
heroVisible: isVisible(heroVideo),
introBeforeHero: comesBefore(introEnd, heroVideo),
heroBeforeInstall: comesBefore(heroVideo, installCTA),
introBeforeInstall: comesBefore(introEnd, installCTA),
installCTAText: text(installCTA),
installCTAHref: installCTA ? installCTA.getAttribute('href') : '',
installCTAVisible: isVisible(installCTA),
};
});
record('public mobile: nav wraps below the logo without clipping and keeps neutral actions',
Expand All @@ -56,14 +87,19 @@ try {
publicState.flexWrap === 'wrap' && publicState.linksBelowLogo &&
publicState.linksInsideViewport && publicState.noHorizontalOverflow,
JSON.stringify(publicState));
record('public mobile: home renders the required hierarchy with a coherent self-hosted recipe',
JSON.stringify(publicState.routes) === JSON.stringify([
'local-agent', 'local-human', 'direct-remote', 'self-hosted-browser', 'hosted-relay',
]) &&
publicState.selfHostedHref === '/patterns/personal-remote-wing/INSTRUCTIONS.md' &&
publicState.selfHostedText.includes('wt serve --local --https') &&
!publicState.selfHostedText.includes('wt roost start --https'),
record('public mobile: home keeps the concise local-agent hierarchy without the detailed route map',
publicState.headingCount === 1 &&
/\blocal\b/i.test(publicState.introText) && /\bagents?\b/i.test(publicState.introText) &&
publicState.routeMapCount === 0 && publicState.dataRouteCount === 0 &&
publicState.patternsHref === '/patterns' && publicState.patternsVisible &&
(!publicState.heroConfigured || (publicState.heroVisible && publicState.introBeforeHero && publicState.heroBeforeInstall)) &&
publicState.introBeforeInstall && publicState.installCTAVisible &&
/\binstall\b/i.test(publicState.installCTAText) && Boolean(publicState.installCTAHref),
JSON.stringify(publicState));
const patternsResponse = await publicContext.request.get(BASE + publicState.patternsHref);
record('public mobile: patterns remains the reachable detailed-route destination',
publicState.patternsHref === '/patterns' && patternsResponse.ok(),
JSON.stringify({ href: publicState.patternsHref, status: patternsResponse.status() }));
await publicPage.screenshot({ path: `${OUT}/public-mobile-nav.png`, fullPage: false });
await publicContext.close();

Expand Down
Loading