agents: shareable per-agent URLs (0.90.0)
opening an agent only lived in component state, so you couldn't link to one, and a refresh dropped the open agent — inconsistent with the now-shareable docs. each agent modal now has a URL: amerc.ai/#/agents/<slug>. - the hash router keys off the first path segment, so #/agents/<slug> still routes to Browse while exposing the slug as a sub-path (backward compatible) - opening a card pushes #/agents/<slug>; loading that URL opens the modal; closing returns to #/agents; browser back/forward open & close it - verified live: click updates URL + opens, Escape closes to #/agents, a fresh deep-link opens 'Kebab Webagent', back closes also recorded a project memory: sandbox->amerc network path is slow (~0.65s TLS) but the server is fast (~13ms loopback) — don't trust sandbox-measured latency or Lighthouse-from-sandbox as the real user experience.
This commit is contained in:
parent
cd9c7f7e8c
commit
0fd1ed2b5c
@ -17,7 +17,7 @@
|
||||
@media (prefers-reduced-motion: reduce) { .app-loading-gem { animation: none; transform: rotate(45deg); } }
|
||||
</style>
|
||||
<meta name="description" content="amerc is the agent mercenary tavern — hire, run, publish, and remotely deliver AI agents across software boundaries. No infrastructure, just skills." />
|
||||
<meta name="version" content="0.89.0-changelog" />
|
||||
<meta name="version" content="0.90.0-agenturls" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amerc-site",
|
||||
"version": "0.89.0-changelog",
|
||||
"version": "0.90.0-agenturls",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { api } from './api.js';
|
||||
import { copyToast } from './toast.js';
|
||||
|
||||
@ -27,6 +27,36 @@ export function AgentBrowse({ auth, setRoute }) {
|
||||
}, [tag, q]);
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
// shareable agent URLs: #/agents/<slug> opens that agent's modal (bookmark/share/refresh-safe)
|
||||
const didInit = useRef(false);
|
||||
const openClass = useCallback((c) => {
|
||||
setOpen(c.id);
|
||||
if (c.slug) window.history.pushState(null, '', `${window.location.pathname}${window.location.search}#/agents/${c.slug}`);
|
||||
}, []);
|
||||
const closeClass = useCallback(() => {
|
||||
setOpen(null);
|
||||
if (window.location.hash !== '#/agents') window.history.pushState(null, '', `${window.location.pathname}${window.location.search}#/agents`);
|
||||
load();
|
||||
}, [load]);
|
||||
useEffect(() => {
|
||||
if (didInit.current || !loaded) return;
|
||||
didInit.current = true;
|
||||
const slug = window.location.hash.replace(/^#\/?/, '').split('/')[1];
|
||||
if (slug) { const hit = classes.find((x) => x.slug === slug); if (hit) setOpen(hit.id); }
|
||||
}, [loaded, classes]);
|
||||
useEffect(() => {
|
||||
const onHash = () => {
|
||||
const parts = window.location.hash.replace(/^#\/?/, '').split('/');
|
||||
if (parts[0] !== 'agents') return;
|
||||
if (!parts[1]) { setOpen(null); return; }
|
||||
const hit = classes.find((x) => x.slug === parts[1]);
|
||||
if (hit) setOpen(hit.id);
|
||||
};
|
||||
window.addEventListener('hashchange', onHash);
|
||||
window.addEventListener('popstate', onHash);
|
||||
return () => { window.removeEventListener('hashchange', onHash); window.removeEventListener('popstate', onHash); };
|
||||
}, [classes]);
|
||||
|
||||
return (
|
||||
<div className="ap">
|
||||
<div className="ap-store-head">
|
||||
@ -52,7 +82,7 @@ export function AgentBrowse({ auth, setRoute }) {
|
||||
</div>
|
||||
))}
|
||||
{loaded && classes.map((c) => (
|
||||
<button key={c.id} className="ap-card" onClick={() => setOpen(c.id)}>
|
||||
<button key={c.id} className="ap-card" onClick={() => openClass(c)}>
|
||||
<div className="ap-card-head2">
|
||||
<span className="ap-avatar" style={avatarStyle(c.name)}>{classEmoji(c)}</span>
|
||||
<div className="ap-card-titles"><strong>{c.name}</strong><span className="ap-kind">{c.kind}</span></div>
|
||||
@ -93,7 +123,7 @@ export function AgentBrowse({ auth, setRoute }) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{open && <ClassModal id={open} auth={auth} onClose={() => { setOpen(null); load(); }} />}
|
||||
{open && <ClassModal id={open} auth={auth} onClose={closeClass} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import { api } from './api.js';
|
||||
const Menu = ({ size = 20 }) => (<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" /></svg>);
|
||||
const X = ({ size = 20 }) => (<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>);
|
||||
|
||||
const RELEASE = '0.89.0-changelog';
|
||||
const RELEASE = '0.90.0-agenturls';
|
||||
|
||||
const NAV = [
|
||||
{ id: 'home', label: 'Tavern' },
|
||||
@ -27,8 +27,9 @@ const ROUTE_TITLES = {
|
||||
|
||||
function useHashRoute() {
|
||||
const normalize = () => {
|
||||
const raw = window.location.hash.replace(/^#\/?/, '');
|
||||
return VALID_ROUTES.includes(raw) ? raw : 'home';
|
||||
// first path segment is the route; the rest (e.g. #/agents/<slug>) is a sub-path
|
||||
const base = window.location.hash.replace(/^#\/?/, '').split('/')[0];
|
||||
return VALID_ROUTES.includes(base) ? base : 'home';
|
||||
};
|
||||
const [route, setRouteState] = useState(normalize);
|
||||
useEffect(() => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user