@guuey/create-agentic-app 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/templates/agentic-app/claude-agent-sdk/README.md +6 -4
- package/dist/templates/agentic-app/claude-agent-sdk/package.json +3 -3
- package/dist/templates/agentic-app/claude-agent-sdk/scripts/bootstrap.mjs +22 -0
- package/dist/templates/agentic-app/claude-agent-sdk/web/package.json +2 -1
- package/dist/templates/agentic-app/claude-agent-sdk/web/src/components/AgentChat.tsx +46 -3
- package/dist/templates/agentic-app/claude-agent-sdk/web/src/components/AppShell.tsx +109 -21
- package/dist/templates/agentic-app/claude-agent-sdk/web/src/components/QrLink.tsx +25 -0
- package/dist/templates/agentic-app/claude-agent-sdk/web/src/main.tsx +2 -3
- package/dist/templates/agentic-app/claude-agent-sdk/web/src/pages/Dashboard.tsx +2 -5
- package/dist/templates/agentic-app/claude-agent-sdk/web/src/pages/Landing.tsx +17 -2
- package/dist/templates/agentic-app/claude-agent-sdk/web/src/pages/TalkOnMobile.tsx +2 -18
- package/dist/templates/agentic-app/claude-agent-sdk/web/src/routes.ts +3 -3
- package/dist/templates/agentic-app/claude-agent-sdk/web/src/styles-app.css +22 -24
- package/dist/templates/agentic-app/claude-agent-sdk/web/src/vite-env.d.ts +13 -0
- package/dist/templates/agentic-app/google-adk/package.json +2 -2
- package/dist/templates/agentic-app/google-adk/scripts/bootstrap.mjs +22 -0
- package/dist/templates/agentic-app/google-adk/web/package.json +2 -1
- package/dist/templates/agentic-app/google-adk/web/src/components/AgentChat.tsx +46 -3
- package/dist/templates/agentic-app/google-adk/web/src/components/AppShell.tsx +109 -21
- package/dist/templates/agentic-app/google-adk/web/src/components/QrLink.tsx +25 -0
- package/dist/templates/agentic-app/google-adk/web/src/main.tsx +2 -3
- package/dist/templates/agentic-app/google-adk/web/src/pages/Dashboard.tsx +2 -5
- package/dist/templates/agentic-app/google-adk/web/src/pages/Landing.tsx +17 -2
- package/dist/templates/agentic-app/google-adk/web/src/pages/TalkOnMobile.tsx +2 -18
- package/dist/templates/agentic-app/google-adk/web/src/routes.ts +3 -3
- package/dist/templates/agentic-app/google-adk/web/src/styles-app.css +22 -24
- package/dist/templates/agentic-app/google-adk/web/src/vite-env.d.ts +13 -0
- package/dist/templates/agentic-app/openai-agents-sdk/README.md +6 -4
- package/dist/templates/agentic-app/openai-agents-sdk/package.json +3 -3
- package/dist/templates/agentic-app/openai-agents-sdk/scripts/bootstrap.mjs +22 -0
- package/dist/templates/agentic-app/openai-agents-sdk/web/package.json +2 -1
- package/dist/templates/agentic-app/openai-agents-sdk/web/src/components/AgentChat.tsx +46 -3
- package/dist/templates/agentic-app/openai-agents-sdk/web/src/components/AppShell.tsx +109 -21
- package/dist/templates/agentic-app/openai-agents-sdk/web/src/components/QrLink.tsx +25 -0
- package/dist/templates/agentic-app/openai-agents-sdk/web/src/main.tsx +2 -3
- package/dist/templates/agentic-app/openai-agents-sdk/web/src/pages/Dashboard.tsx +2 -5
- package/dist/templates/agentic-app/openai-agents-sdk/web/src/pages/Landing.tsx +17 -2
- package/dist/templates/agentic-app/openai-agents-sdk/web/src/pages/TalkOnMobile.tsx +2 -18
- package/dist/templates/agentic-app/openai-agents-sdk/web/src/routes.ts +3 -3
- package/dist/templates/agentic-app/openai-agents-sdk/web/src/styles-app.css +22 -24
- package/dist/templates/agentic-app/openai-agents-sdk/web/src/vite-env.d.ts +13 -0
- package/dist/templates/base/claude-agent-sdk/README.md +6 -4
- package/dist/templates/base/claude-agent-sdk/package.json +3 -3
- package/dist/templates/base/claude-agent-sdk/scripts/bootstrap.mjs +22 -0
- package/dist/templates/base/claude-agent-sdk/web/package.json +3 -1
- package/dist/templates/base/claude-agent-sdk/web/src/components/AgentChat.tsx +46 -3
- package/dist/templates/base/claude-agent-sdk/web/src/components/QrLink.tsx +25 -0
- package/dist/templates/base/claude-agent-sdk/web/src/main.tsx +14 -0
- package/dist/templates/base/claude-agent-sdk/web/src/pages/Landing.tsx +17 -2
- package/dist/templates/base/claude-agent-sdk/web/src/vite-env.d.ts +13 -0
- package/dist/templates/base/google-adk/package.json +2 -2
- package/dist/templates/base/google-adk/scripts/bootstrap.mjs +22 -0
- package/dist/templates/base/google-adk/web/package.json +3 -1
- package/dist/templates/base/google-adk/web/src/components/AgentChat.tsx +46 -3
- package/dist/templates/base/google-adk/web/src/components/QrLink.tsx +25 -0
- package/dist/templates/base/google-adk/web/src/main.tsx +14 -0
- package/dist/templates/base/google-adk/web/src/pages/Landing.tsx +17 -2
- package/dist/templates/base/google-adk/web/src/vite-env.d.ts +13 -0
- package/dist/templates/base/openai-agents-sdk/README.md +6 -4
- package/dist/templates/base/openai-agents-sdk/package.json +3 -3
- package/dist/templates/base/openai-agents-sdk/scripts/bootstrap.mjs +22 -0
- package/dist/templates/base/openai-agents-sdk/web/package.json +3 -1
- package/dist/templates/base/openai-agents-sdk/web/src/components/AgentChat.tsx +46 -3
- package/dist/templates/base/openai-agents-sdk/web/src/components/QrLink.tsx +25 -0
- package/dist/templates/base/openai-agents-sdk/web/src/main.tsx +14 -0
- package/dist/templates/base/openai-agents-sdk/web/src/pages/Landing.tsx +17 -2
- package/dist/templates/base/openai-agents-sdk/web/src/vite-env.d.ts +13 -0
- package/package.json +3 -3
- package/dist/templates/agentic-app/claude-agent-sdk/web/src/pages/AgentCanvas.tsx +0 -14
- package/dist/templates/agentic-app/google-adk/web/src/pages/AgentCanvas.tsx +0 -14
- package/dist/templates/agentic-app/openai-agents-sdk/web/src/pages/AgentCanvas.tsx +0 -14
|
@@ -8,6 +8,7 @@ import { Link } from "react-router-dom";
|
|
|
8
8
|
import { appConfig, isLinked } from "../config";
|
|
9
9
|
import { CHAT_PATH, HOME_PATH } from "../routes";
|
|
10
10
|
import { mountWidget, type WidgetOutcome } from "../lib/widget";
|
|
11
|
+
import { QrLink } from "../components/QrLink";
|
|
11
12
|
|
|
12
13
|
export function Landing() {
|
|
13
14
|
const [widget, setWidget] = useState<WidgetOutcome | "unlinked" | "loading">(
|
|
@@ -19,19 +20,33 @@ export function Landing() {
|
|
|
19
20
|
mountWidget((outcome) => setWidget(outcome));
|
|
20
21
|
}, []);
|
|
21
22
|
|
|
23
|
+
// The demo posture (spec §2.3): a REAL product site whose hero's one job
|
|
24
|
+
// is getting the visitor INTO the demo — big CTA to the console page +
|
|
25
|
+
// a QR to continue on mobile via the portal.
|
|
26
|
+
const portalTarget =
|
|
27
|
+
appConfig.link !== null
|
|
28
|
+
? `${appConfig.link.portalUrl}/agent/${appConfig.link.slug ?? appConfig.link.appId}`
|
|
29
|
+
: null;
|
|
30
|
+
|
|
22
31
|
return (
|
|
23
32
|
<main className="page landing">
|
|
24
|
-
<section className="hero">
|
|
33
|
+
<section className="hero" data-tour="hero">
|
|
25
34
|
<h1>{appConfig.copy.landing.headline}</h1>
|
|
26
35
|
<p>{appConfig.copy.landing.sub}</p>
|
|
27
36
|
<div className="hero-actions">
|
|
28
37
|
<Link to={CHAT_PATH} className="btn btn-accent">
|
|
29
|
-
Open the chat
|
|
38
|
+
{appConfig.demoMode ? "Enter the demo" : "Open the chat"}
|
|
30
39
|
</Link>
|
|
31
40
|
<Link to={HOME_PATH} className="btn">
|
|
32
41
|
How this app is wired
|
|
33
42
|
</Link>
|
|
34
43
|
</div>
|
|
44
|
+
{appConfig.demoMode && portalTarget !== null ? (
|
|
45
|
+
<div className="hero-qr">
|
|
46
|
+
<p className="hint">Or continue on your phone:</p>
|
|
47
|
+
<QrLink url={portalTarget} size={140} />
|
|
48
|
+
</div>
|
|
49
|
+
) : null}
|
|
35
50
|
{widget === "unlinked" ? (
|
|
36
51
|
<p className="hint">
|
|
37
52
|
The floating agent launcher appears here once the app is bound to a
|
|
@@ -1 +1,14 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
interface ImportMetaEnv {
|
|
4
|
+
/**
|
|
5
|
+
* Optional external-script hook (guuey#303): a URL loaded at boot — the
|
|
6
|
+
* demo tour's bundle rides this in the demo apps; unset in customer
|
|
7
|
+
* builds.
|
|
8
|
+
*/
|
|
9
|
+
readonly VITE_DEMO_TOUR_SRC?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ImportMeta {
|
|
13
|
+
readonly env: ImportMetaEnv;
|
|
14
|
+
}
|
|
@@ -121,6 +121,27 @@ async function guuey(args) {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// ── ggui.json theme sync (guuey#302 scaffold hygiene) ───────────────────────
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Keep `ggui/ggui.json`'s theme MODE in step with guuey.app.json so the
|
|
128
|
+
* local `ggui serve` preview matches the site (the scaffold used to ship
|
|
129
|
+
* the two contradicting each other: light site, dark ggui preview). Only
|
|
130
|
+
* `mode` syncs — `preset` is ggui-side vocabulary and stays the author's
|
|
131
|
+
* choice. NOTE: the deployed render's theme is platform data (`guuey
|
|
132
|
+
* deploy` deliberately ignores this block; guuey#304 owns the production
|
|
133
|
+
* side) — this sync is about local-preview honesty.
|
|
134
|
+
*/
|
|
135
|
+
function syncGguiTheme(config) {
|
|
136
|
+
const gguiJsonPath = join(projectRoot, "ggui", "ggui.json");
|
|
137
|
+
if (!existsSync(gguiJsonPath)) return;
|
|
138
|
+
const ggui = JSON.parse(readFileSync(gguiJsonPath, "utf8"));
|
|
139
|
+
if (ggui.theme?.mode === config.theme.mode) return;
|
|
140
|
+
ggui.theme = { ...(ggui.theme ?? {}), mode: config.theme.mode };
|
|
141
|
+
writeFileSync(gguiJsonPath, `${JSON.stringify(ggui, null, 2)}\n`, "utf8");
|
|
142
|
+
console.log(`ggui/ggui.json theme mode → ${config.theme.mode} (local preview matches the site).`);
|
|
143
|
+
}
|
|
144
|
+
|
|
124
145
|
// ── AGENTS.md managed block ─────────────────────────────────────────────────
|
|
125
146
|
|
|
126
147
|
function managedBlock(config) {
|
|
@@ -367,6 +388,7 @@ async function main() {
|
|
|
367
388
|
// extraction (`--example`), the demo chrome turns off here.
|
|
368
389
|
config.demoMode = false;
|
|
369
390
|
writeConfig(config);
|
|
391
|
+
syncGguiTheme(config);
|
|
370
392
|
regenerateAgentsMd(config);
|
|
371
393
|
|
|
372
394
|
console.log(`\nConfigured "${name}" — guuey.app.json written, AGENTS.md updated.`);
|
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
"typecheck": "tsc --noEmit"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@guuey/chat": "0.
|
|
12
|
+
"@guuey/chat": "0.12.0",
|
|
13
13
|
"oidc-client-ts": "^3.1.0",
|
|
14
|
+
"qrcode": "^1.5.4",
|
|
14
15
|
"react": "^19.0.0",
|
|
15
16
|
"react-dom": "^19.0.0",
|
|
16
17
|
"react-router-dom": "^7.1.0"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
|
19
20
|
"@types/node": "^24.0.0",
|
|
21
|
+
"@types/qrcode": "^1.5.5",
|
|
20
22
|
"@types/react": "^19.0.0",
|
|
21
23
|
"@types/react-dom": "^19.0.0",
|
|
22
24
|
"@vitejs/plugin-react": "^4.3.4",
|
|
@@ -10,11 +10,43 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { useEffect, useState, type CSSProperties } from "react";
|
|
12
12
|
import { GuueyChat } from "@guuey/chat/react";
|
|
13
|
+
import type { GuueyChatHandle } from "@guuey/chat/react";
|
|
14
|
+
import type { PlanViewSummary, ViewRefItem } from "@guuey/chat";
|
|
13
15
|
import { agentEndpointUrl, appConfig, historyBaseUrl } from "../config";
|
|
14
16
|
import { currentIdentityMode, ensureGuestSecret } from "../lib/identity";
|
|
15
17
|
import { getBearerToken, currentUser, oidcConfigured } from "../lib/oidc";
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The chat-rail bridge (agentic-app shell): with `viewsBridge` set, the
|
|
21
|
+
* chat runs the kit's CHIPS presentation — generative views collapse to
|
|
22
|
+
* compact chips in the rail, the full renders belong to the host's main
|
|
23
|
+
* canvas (fed by `onViewsChange`), and chip clicks re-select
|
|
24
|
+
* (`onViewRef` → `promotedViewKey`, the browser-history mechanic).
|
|
25
|
+
*/
|
|
26
|
+
export interface ViewsBridge {
|
|
27
|
+
promotedViewKey?: string | undefined;
|
|
28
|
+
onViewRef: (item: ViewRefItem) => void;
|
|
29
|
+
onViewsChange: (views: PlanViewSummary[]) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Module scope on purpose: props want STABLE identities. An inline
|
|
33
|
+
// `policy={{ … }}` literal (or per-render arrow getters) re-mints every
|
|
34
|
+
// render; the kit stabilizes structurally since 0.11.0, but the template
|
|
35
|
+
// models the correct shape — one identity for the app's whole life.
|
|
36
|
+
const RAIL_POLICY = { view: { timeoutMs: 8000, presentation: "chips" as const } };
|
|
37
|
+
|
|
38
|
+
export function AgentChat({
|
|
39
|
+
className,
|
|
40
|
+
style,
|
|
41
|
+
viewsBridge,
|
|
42
|
+
onReady,
|
|
43
|
+
}: {
|
|
44
|
+
className?: string;
|
|
45
|
+
style?: CSSProperties;
|
|
46
|
+
viewsBridge?: ViewsBridge;
|
|
47
|
+
/** Receives the chat handle — the AppShell mounts canvas views with `handle.viewSlotProps()`. */
|
|
48
|
+
onReady?: (handle: GuueyChatHandle) => void;
|
|
49
|
+
}) {
|
|
18
50
|
// The user's CHOSEN mode wins: an explicit "Continue as guest" must never
|
|
19
51
|
// be shadowed by a cached OIDC session. Only when no choice is recorded
|
|
20
52
|
// does a live session imply oidc.
|
|
@@ -43,11 +75,22 @@ export function AgentChat({ className, style }: { className?: string; style?: CS
|
|
|
43
75
|
mode: appConfig.theme.mode,
|
|
44
76
|
className,
|
|
45
77
|
style,
|
|
78
|
+
...(onReady !== undefined ? { onReady } : {}),
|
|
79
|
+
...(viewsBridge !== undefined
|
|
80
|
+
? {
|
|
81
|
+
policy: RAIL_POLICY,
|
|
82
|
+
promotedViewKey: viewsBridge.promotedViewKey,
|
|
83
|
+
onViewRef: viewsBridge.onViewRef,
|
|
84
|
+
onViewsChange: viewsBridge.onViewsChange,
|
|
85
|
+
}
|
|
86
|
+
: {}),
|
|
46
87
|
};
|
|
47
88
|
|
|
89
|
+
// The lib functions ARE the stable getters — no wrapping arrows (a fresh
|
|
90
|
+
// closure per render is the identity churn RAIL_POLICY exists to avoid).
|
|
48
91
|
return identity === "oidc" ? (
|
|
49
|
-
<GuueyChat {...shared} getAccessToken={
|
|
92
|
+
<GuueyChat {...shared} getAccessToken={getBearerToken} />
|
|
50
93
|
) : (
|
|
51
|
-
<GuueyChat {...shared} getGuestSecret={
|
|
94
|
+
<GuueyChat {...shared} getGuestSecret={ensureGuestSecret} />
|
|
52
95
|
);
|
|
53
96
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** A QR code for a URL, with the plain link beneath — mobile hand-off. */
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import QRCode from "qrcode";
|
|
4
|
+
|
|
5
|
+
export function QrLink({ url, size = 200 }: { url: string; size?: number }) {
|
|
6
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
7
|
+
const [error, setError] = useState<string | null>(null);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!canvasRef.current) return;
|
|
11
|
+
QRCode.toCanvas(canvasRef.current, url, { width: size, margin: 1 }).catch((err: unknown) => {
|
|
12
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
13
|
+
});
|
|
14
|
+
}, [url, size]);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<span className="qr-link">
|
|
18
|
+
<canvas ref={canvasRef} className="qr" />
|
|
19
|
+
<a href={url} target="_blank" rel="noreferrer">
|
|
20
|
+
{url}
|
|
21
|
+
</a>
|
|
22
|
+
{error ? <span className="error">QR render failed: {error}</span> : null}
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -17,6 +17,20 @@ document.documentElement.dataset.mode = appConfig.theme.mode;
|
|
|
17
17
|
document.documentElement.style.setProperty("--app-accent", appConfig.theme.accent);
|
|
18
18
|
document.title = appConfig.brand.name;
|
|
19
19
|
|
|
20
|
+
// Optional external script (the analytics-snippet pattern): a build-time
|
|
21
|
+
// env names a script URL and this app loads it — the demo tour rides this
|
|
22
|
+
// hook (its bundle lives OUTSIDE the template; guuey#303). The script can
|
|
23
|
+
// anchor on the `data-tour` attributes the pages carry and listen for the
|
|
24
|
+
// `demo:render-complete` CustomEvent the app shell dispatches when a
|
|
25
|
+
// generative view first lands on the canvas.
|
|
26
|
+
const tourSrc = import.meta.env.VITE_DEMO_TOUR_SRC;
|
|
27
|
+
if (tourSrc !== undefined && tourSrc !== "") {
|
|
28
|
+
const script = document.createElement("script");
|
|
29
|
+
script.src = tourSrc;
|
|
30
|
+
script.defer = true;
|
|
31
|
+
document.head.appendChild(script);
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
const router = createBrowserRouter([
|
|
21
35
|
{
|
|
22
36
|
element: <Shell />,
|
|
@@ -8,6 +8,7 @@ import { Link } from "react-router-dom";
|
|
|
8
8
|
import { appConfig, isLinked } from "../config";
|
|
9
9
|
import { CHAT_PATH, HOME_PATH } from "../routes";
|
|
10
10
|
import { mountWidget, type WidgetOutcome } from "../lib/widget";
|
|
11
|
+
import { QrLink } from "../components/QrLink";
|
|
11
12
|
|
|
12
13
|
export function Landing() {
|
|
13
14
|
const [widget, setWidget] = useState<WidgetOutcome | "unlinked" | "loading">(
|
|
@@ -19,19 +20,33 @@ export function Landing() {
|
|
|
19
20
|
mountWidget((outcome) => setWidget(outcome));
|
|
20
21
|
}, []);
|
|
21
22
|
|
|
23
|
+
// The demo posture (spec §2.3): a REAL product site whose hero's one job
|
|
24
|
+
// is getting the visitor INTO the demo — big CTA to the console page +
|
|
25
|
+
// a QR to continue on mobile via the portal.
|
|
26
|
+
const portalTarget =
|
|
27
|
+
appConfig.link !== null
|
|
28
|
+
? `${appConfig.link.portalUrl}/agent/${appConfig.link.slug ?? appConfig.link.appId}`
|
|
29
|
+
: null;
|
|
30
|
+
|
|
22
31
|
return (
|
|
23
32
|
<main className="page landing">
|
|
24
|
-
<section className="hero">
|
|
33
|
+
<section className="hero" data-tour="hero">
|
|
25
34
|
<h1>{appConfig.copy.landing.headline}</h1>
|
|
26
35
|
<p>{appConfig.copy.landing.sub}</p>
|
|
27
36
|
<div className="hero-actions">
|
|
28
37
|
<Link to={CHAT_PATH} className="btn btn-accent">
|
|
29
|
-
Open the chat
|
|
38
|
+
{appConfig.demoMode ? "Enter the demo" : "Open the chat"}
|
|
30
39
|
</Link>
|
|
31
40
|
<Link to={HOME_PATH} className="btn">
|
|
32
41
|
How this app is wired
|
|
33
42
|
</Link>
|
|
34
43
|
</div>
|
|
44
|
+
{appConfig.demoMode && portalTarget !== null ? (
|
|
45
|
+
<div className="hero-qr">
|
|
46
|
+
<p className="hint">Or continue on your phone:</p>
|
|
47
|
+
<QrLink url={portalTarget} size={140} />
|
|
48
|
+
</div>
|
|
49
|
+
) : null}
|
|
35
50
|
{widget === "unlinked" ? (
|
|
36
51
|
<p className="hint">
|
|
37
52
|
The floating agent launcher appears here once the app is bound to a
|
|
@@ -1 +1,14 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
interface ImportMetaEnv {
|
|
4
|
+
/**
|
|
5
|
+
* Optional external-script hook (guuey#303): a URL loaded at boot — the
|
|
6
|
+
* demo tour's bundle rides this in the demo apps; unset in customer
|
|
7
|
+
* builds.
|
|
8
|
+
*/
|
|
9
|
+
readonly VITE_DEMO_TOUR_SRC?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ImportMeta {
|
|
13
|
+
readonly env: ImportMetaEnv;
|
|
14
|
+
}
|
|
@@ -15,9 +15,11 @@ locally with one command, and deployable to guuey with one more.
|
|
|
15
15
|
├── prompts/system.md # system prompt, referenced from guuey.json#agent.systemPrompt.file
|
|
16
16
|
├── mcps/todo/ # the "copy-me" custom MCP server — @modelcontextprotocol/sdk,
|
|
17
17
|
│ # Streamable HTTP, in-memory todo list, port :6782
|
|
18
|
-
├── ggui/ # ggui.json + blueprints/
|
|
19
|
-
│ # `ggui serve` runs against this locally;
|
|
20
|
-
│ #
|
|
18
|
+
├── ggui/ # ggui.json + blueprints/ — generative-UI config.
|
|
19
|
+
│ # `ggui serve` runs against this locally; `guuey deploy` pushes
|
|
20
|
+
│ # generation config + blueprints (the deployed RENDER THEME is
|
|
21
|
+
│ # platform data, not this dir — ggui.json#theme is local-preview
|
|
22
|
+
│ # only, kept in step with guuey.app.json by `pnpm bootstrap`).
|
|
21
23
|
├── web/ # the product frontend (Vite + React on @guuey/chat):
|
|
22
24
|
│ # landing (widget) · login (guest + BYO-OIDC seam) · home
|
|
23
25
|
│ # (status + distribution guide) · the chat surface
|
|
@@ -127,7 +129,7 @@ anything user-visible changes:
|
|
|
127
129
|
as its own hosted MCP server (build → deploy → registry), then writes the
|
|
128
130
|
resulting server id back into `guuey.json`. This scaffold has none — the
|
|
129
131
|
todo MCP is `colocated`, so this leg is a no-op here.
|
|
130
|
-
2. **ggui asset leg** — pushes `ggui/` (
|
|
132
|
+
2. **ggui asset leg** — pushes `ggui/` (generation config, blueprints) to your
|
|
131
133
|
app's guuey-managed ggui instance.
|
|
132
134
|
3. **Agent leg** — builds `src/worker.ts` into `guuey.worker.js`, packs the
|
|
133
135
|
project root (including `mcps/`), and deploys it as a gVisor-isolated,
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@openai/agents": "^0.12.0",
|
|
15
|
-
"@guuey/config": "0.
|
|
16
|
-
"@guuey/worker": "0.
|
|
15
|
+
"@guuey/config": "0.12.0",
|
|
16
|
+
"@guuey/worker": "0.12.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@ggui-ai/cli": "0.6.3",
|
|
20
|
-
"@guuey/cli": "0.
|
|
20
|
+
"@guuey/cli": "0.12.0",
|
|
21
21
|
"tsup": "^8.5.0",
|
|
22
22
|
"tsx": "^4.19.0",
|
|
23
23
|
"typescript": "^5.8.0"
|
|
@@ -121,6 +121,27 @@ async function guuey(args) {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// ── ggui.json theme sync (guuey#302 scaffold hygiene) ───────────────────────
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Keep `ggui/ggui.json`'s theme MODE in step with guuey.app.json so the
|
|
128
|
+
* local `ggui serve` preview matches the site (the scaffold used to ship
|
|
129
|
+
* the two contradicting each other: light site, dark ggui preview). Only
|
|
130
|
+
* `mode` syncs — `preset` is ggui-side vocabulary and stays the author's
|
|
131
|
+
* choice. NOTE: the deployed render's theme is platform data (`guuey
|
|
132
|
+
* deploy` deliberately ignores this block; guuey#304 owns the production
|
|
133
|
+
* side) — this sync is about local-preview honesty.
|
|
134
|
+
*/
|
|
135
|
+
function syncGguiTheme(config) {
|
|
136
|
+
const gguiJsonPath = join(projectRoot, "ggui", "ggui.json");
|
|
137
|
+
if (!existsSync(gguiJsonPath)) return;
|
|
138
|
+
const ggui = JSON.parse(readFileSync(gguiJsonPath, "utf8"));
|
|
139
|
+
if (ggui.theme?.mode === config.theme.mode) return;
|
|
140
|
+
ggui.theme = { ...(ggui.theme ?? {}), mode: config.theme.mode };
|
|
141
|
+
writeFileSync(gguiJsonPath, `${JSON.stringify(ggui, null, 2)}\n`, "utf8");
|
|
142
|
+
console.log(`ggui/ggui.json theme mode → ${config.theme.mode} (local preview matches the site).`);
|
|
143
|
+
}
|
|
144
|
+
|
|
124
145
|
// ── AGENTS.md managed block ─────────────────────────────────────────────────
|
|
125
146
|
|
|
126
147
|
function managedBlock(config) {
|
|
@@ -367,6 +388,7 @@ async function main() {
|
|
|
367
388
|
// extraction (`--example`), the demo chrome turns off here.
|
|
368
389
|
config.demoMode = false;
|
|
369
390
|
writeConfig(config);
|
|
391
|
+
syncGguiTheme(config);
|
|
370
392
|
regenerateAgentsMd(config);
|
|
371
393
|
|
|
372
394
|
console.log(`\nConfigured "${name}" — guuey.app.json written, AGENTS.md updated.`);
|
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
"typecheck": "tsc --noEmit"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@guuey/chat": "0.
|
|
12
|
+
"@guuey/chat": "0.12.0",
|
|
13
13
|
"oidc-client-ts": "^3.1.0",
|
|
14
|
+
"qrcode": "^1.5.4",
|
|
14
15
|
"react": "^19.0.0",
|
|
15
16
|
"react-dom": "^19.0.0",
|
|
16
17
|
"react-router-dom": "^7.1.0"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
|
19
20
|
"@types/node": "^24.0.0",
|
|
21
|
+
"@types/qrcode": "^1.5.5",
|
|
20
22
|
"@types/react": "^19.0.0",
|
|
21
23
|
"@types/react-dom": "^19.0.0",
|
|
22
24
|
"@vitejs/plugin-react": "^4.3.4",
|
|
@@ -10,11 +10,43 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { useEffect, useState, type CSSProperties } from "react";
|
|
12
12
|
import { GuueyChat } from "@guuey/chat/react";
|
|
13
|
+
import type { GuueyChatHandle } from "@guuey/chat/react";
|
|
14
|
+
import type { PlanViewSummary, ViewRefItem } from "@guuey/chat";
|
|
13
15
|
import { agentEndpointUrl, appConfig, historyBaseUrl } from "../config";
|
|
14
16
|
import { currentIdentityMode, ensureGuestSecret } from "../lib/identity";
|
|
15
17
|
import { getBearerToken, currentUser, oidcConfigured } from "../lib/oidc";
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The chat-rail bridge (agentic-app shell): with `viewsBridge` set, the
|
|
21
|
+
* chat runs the kit's CHIPS presentation — generative views collapse to
|
|
22
|
+
* compact chips in the rail, the full renders belong to the host's main
|
|
23
|
+
* canvas (fed by `onViewsChange`), and chip clicks re-select
|
|
24
|
+
* (`onViewRef` → `promotedViewKey`, the browser-history mechanic).
|
|
25
|
+
*/
|
|
26
|
+
export interface ViewsBridge {
|
|
27
|
+
promotedViewKey?: string | undefined;
|
|
28
|
+
onViewRef: (item: ViewRefItem) => void;
|
|
29
|
+
onViewsChange: (views: PlanViewSummary[]) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Module scope on purpose: props want STABLE identities. An inline
|
|
33
|
+
// `policy={{ … }}` literal (or per-render arrow getters) re-mints every
|
|
34
|
+
// render; the kit stabilizes structurally since 0.11.0, but the template
|
|
35
|
+
// models the correct shape — one identity for the app's whole life.
|
|
36
|
+
const RAIL_POLICY = { view: { timeoutMs: 8000, presentation: "chips" as const } };
|
|
37
|
+
|
|
38
|
+
export function AgentChat({
|
|
39
|
+
className,
|
|
40
|
+
style,
|
|
41
|
+
viewsBridge,
|
|
42
|
+
onReady,
|
|
43
|
+
}: {
|
|
44
|
+
className?: string;
|
|
45
|
+
style?: CSSProperties;
|
|
46
|
+
viewsBridge?: ViewsBridge;
|
|
47
|
+
/** Receives the chat handle — the AppShell mounts canvas views with `handle.viewSlotProps()`. */
|
|
48
|
+
onReady?: (handle: GuueyChatHandle) => void;
|
|
49
|
+
}) {
|
|
18
50
|
// The user's CHOSEN mode wins: an explicit "Continue as guest" must never
|
|
19
51
|
// be shadowed by a cached OIDC session. Only when no choice is recorded
|
|
20
52
|
// does a live session imply oidc.
|
|
@@ -43,11 +75,22 @@ export function AgentChat({ className, style }: { className?: string; style?: CS
|
|
|
43
75
|
mode: appConfig.theme.mode,
|
|
44
76
|
className,
|
|
45
77
|
style,
|
|
78
|
+
...(onReady !== undefined ? { onReady } : {}),
|
|
79
|
+
...(viewsBridge !== undefined
|
|
80
|
+
? {
|
|
81
|
+
policy: RAIL_POLICY,
|
|
82
|
+
promotedViewKey: viewsBridge.promotedViewKey,
|
|
83
|
+
onViewRef: viewsBridge.onViewRef,
|
|
84
|
+
onViewsChange: viewsBridge.onViewsChange,
|
|
85
|
+
}
|
|
86
|
+
: {}),
|
|
46
87
|
};
|
|
47
88
|
|
|
89
|
+
// The lib functions ARE the stable getters — no wrapping arrows (a fresh
|
|
90
|
+
// closure per render is the identity churn RAIL_POLICY exists to avoid).
|
|
48
91
|
return identity === "oidc" ? (
|
|
49
|
-
<GuueyChat {...shared} getAccessToken={
|
|
92
|
+
<GuueyChat {...shared} getAccessToken={getBearerToken} />
|
|
50
93
|
) : (
|
|
51
|
-
<GuueyChat {...shared} getGuestSecret={
|
|
94
|
+
<GuueyChat {...shared} getGuestSecret={ensureGuestSecret} />
|
|
52
95
|
);
|
|
53
96
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** A QR code for a URL, with the plain link beneath — mobile hand-off. */
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import QRCode from "qrcode";
|
|
4
|
+
|
|
5
|
+
export function QrLink({ url, size = 200 }: { url: string; size?: number }) {
|
|
6
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
7
|
+
const [error, setError] = useState<string | null>(null);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!canvasRef.current) return;
|
|
11
|
+
QRCode.toCanvas(canvasRef.current, url, { width: size, margin: 1 }).catch((err: unknown) => {
|
|
12
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
13
|
+
});
|
|
14
|
+
}, [url, size]);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<span className="qr-link">
|
|
18
|
+
<canvas ref={canvasRef} className="qr" />
|
|
19
|
+
<a href={url} target="_blank" rel="noreferrer">
|
|
20
|
+
{url}
|
|
21
|
+
</a>
|
|
22
|
+
{error ? <span className="error">QR render failed: {error}</span> : null}
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -17,6 +17,20 @@ document.documentElement.dataset.mode = appConfig.theme.mode;
|
|
|
17
17
|
document.documentElement.style.setProperty("--app-accent", appConfig.theme.accent);
|
|
18
18
|
document.title = appConfig.brand.name;
|
|
19
19
|
|
|
20
|
+
// Optional external script (the analytics-snippet pattern): a build-time
|
|
21
|
+
// env names a script URL and this app loads it — the demo tour rides this
|
|
22
|
+
// hook (its bundle lives OUTSIDE the template; guuey#303). The script can
|
|
23
|
+
// anchor on the `data-tour` attributes the pages carry and listen for the
|
|
24
|
+
// `demo:render-complete` CustomEvent the app shell dispatches when a
|
|
25
|
+
// generative view first lands on the canvas.
|
|
26
|
+
const tourSrc = import.meta.env.VITE_DEMO_TOUR_SRC;
|
|
27
|
+
if (tourSrc !== undefined && tourSrc !== "") {
|
|
28
|
+
const script = document.createElement("script");
|
|
29
|
+
script.src = tourSrc;
|
|
30
|
+
script.defer = true;
|
|
31
|
+
document.head.appendChild(script);
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
const router = createBrowserRouter([
|
|
21
35
|
{
|
|
22
36
|
element: <Shell />,
|
|
@@ -8,6 +8,7 @@ import { Link } from "react-router-dom";
|
|
|
8
8
|
import { appConfig, isLinked } from "../config";
|
|
9
9
|
import { CHAT_PATH, HOME_PATH } from "../routes";
|
|
10
10
|
import { mountWidget, type WidgetOutcome } from "../lib/widget";
|
|
11
|
+
import { QrLink } from "../components/QrLink";
|
|
11
12
|
|
|
12
13
|
export function Landing() {
|
|
13
14
|
const [widget, setWidget] = useState<WidgetOutcome | "unlinked" | "loading">(
|
|
@@ -19,19 +20,33 @@ export function Landing() {
|
|
|
19
20
|
mountWidget((outcome) => setWidget(outcome));
|
|
20
21
|
}, []);
|
|
21
22
|
|
|
23
|
+
// The demo posture (spec §2.3): a REAL product site whose hero's one job
|
|
24
|
+
// is getting the visitor INTO the demo — big CTA to the console page +
|
|
25
|
+
// a QR to continue on mobile via the portal.
|
|
26
|
+
const portalTarget =
|
|
27
|
+
appConfig.link !== null
|
|
28
|
+
? `${appConfig.link.portalUrl}/agent/${appConfig.link.slug ?? appConfig.link.appId}`
|
|
29
|
+
: null;
|
|
30
|
+
|
|
22
31
|
return (
|
|
23
32
|
<main className="page landing">
|
|
24
|
-
<section className="hero">
|
|
33
|
+
<section className="hero" data-tour="hero">
|
|
25
34
|
<h1>{appConfig.copy.landing.headline}</h1>
|
|
26
35
|
<p>{appConfig.copy.landing.sub}</p>
|
|
27
36
|
<div className="hero-actions">
|
|
28
37
|
<Link to={CHAT_PATH} className="btn btn-accent">
|
|
29
|
-
Open the chat
|
|
38
|
+
{appConfig.demoMode ? "Enter the demo" : "Open the chat"}
|
|
30
39
|
</Link>
|
|
31
40
|
<Link to={HOME_PATH} className="btn">
|
|
32
41
|
How this app is wired
|
|
33
42
|
</Link>
|
|
34
43
|
</div>
|
|
44
|
+
{appConfig.demoMode && portalTarget !== null ? (
|
|
45
|
+
<div className="hero-qr">
|
|
46
|
+
<p className="hint">Or continue on your phone:</p>
|
|
47
|
+
<QrLink url={portalTarget} size={140} />
|
|
48
|
+
</div>
|
|
49
|
+
) : null}
|
|
35
50
|
{widget === "unlinked" ? (
|
|
36
51
|
<p className="hint">
|
|
37
52
|
The floating agent launcher appears here once the app is bound to a
|
|
@@ -1 +1,14 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
interface ImportMetaEnv {
|
|
4
|
+
/**
|
|
5
|
+
* Optional external-script hook (guuey#303): a URL loaded at boot — the
|
|
6
|
+
* demo tour's bundle rides this in the demo apps; unset in customer
|
|
7
|
+
* builds.
|
|
8
|
+
*/
|
|
9
|
+
readonly VITE_DEMO_TOUR_SRC?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ImportMeta {
|
|
13
|
+
readonly env: ImportMetaEnv;
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guuey/create-agentic-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Scaffold a guuey agentic app: code-mode agent + custom MCP + ggui + web, local pnpm dev, one-command guuey deploy.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"tsup": "^8.5.0",
|
|
26
26
|
"typescript": "^5.8.0",
|
|
27
27
|
"vitest": "^3.2.4",
|
|
28
|
-
"@guuey/
|
|
29
|
-
"@guuey/
|
|
28
|
+
"@guuey/worker": "0.12.0",
|
|
29
|
+
"@guuey/config": "0.12.0"
|
|
30
30
|
},
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"access": "public"
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The fullscreen agent — the whole main canvas is the chat, so generative
|
|
3
|
-
* UI cards render at real size. This is the same configured `<AgentChat>`
|
|
4
|
-
* every chat surface in this template uses.
|
|
5
|
-
*/
|
|
6
|
-
import { AgentChat } from "../components/AgentChat";
|
|
7
|
-
|
|
8
|
-
export function AgentCanvas() {
|
|
9
|
-
return (
|
|
10
|
-
<div className="agent-canvas">
|
|
11
|
-
<AgentChat className="chat-fill" />
|
|
12
|
-
</div>
|
|
13
|
-
);
|
|
14
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The fullscreen agent — the whole main canvas is the chat, so generative
|
|
3
|
-
* UI cards render at real size. This is the same configured `<AgentChat>`
|
|
4
|
-
* every chat surface in this template uses.
|
|
5
|
-
*/
|
|
6
|
-
import { AgentChat } from "../components/AgentChat";
|
|
7
|
-
|
|
8
|
-
export function AgentCanvas() {
|
|
9
|
-
return (
|
|
10
|
-
<div className="agent-canvas">
|
|
11
|
-
<AgentChat className="chat-fill" />
|
|
12
|
-
</div>
|
|
13
|
-
);
|
|
14
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The fullscreen agent — the whole main canvas is the chat, so generative
|
|
3
|
-
* UI cards render at real size. This is the same configured `<AgentChat>`
|
|
4
|
-
* every chat surface in this template uses.
|
|
5
|
-
*/
|
|
6
|
-
import { AgentChat } from "../components/AgentChat";
|
|
7
|
-
|
|
8
|
-
export function AgentCanvas() {
|
|
9
|
-
return (
|
|
10
|
-
<div className="agent-canvas">
|
|
11
|
-
<AgentChat className="chat-fill" />
|
|
12
|
-
</div>
|
|
13
|
-
);
|
|
14
|
-
}
|