@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.
Files changed (71) hide show
  1. package/dist/templates/agentic-app/claude-agent-sdk/README.md +6 -4
  2. package/dist/templates/agentic-app/claude-agent-sdk/package.json +3 -3
  3. package/dist/templates/agentic-app/claude-agent-sdk/scripts/bootstrap.mjs +22 -0
  4. package/dist/templates/agentic-app/claude-agent-sdk/web/package.json +2 -1
  5. package/dist/templates/agentic-app/claude-agent-sdk/web/src/components/AgentChat.tsx +46 -3
  6. package/dist/templates/agentic-app/claude-agent-sdk/web/src/components/AppShell.tsx +109 -21
  7. package/dist/templates/agentic-app/claude-agent-sdk/web/src/components/QrLink.tsx +25 -0
  8. package/dist/templates/agentic-app/claude-agent-sdk/web/src/main.tsx +2 -3
  9. package/dist/templates/agentic-app/claude-agent-sdk/web/src/pages/Dashboard.tsx +2 -5
  10. package/dist/templates/agentic-app/claude-agent-sdk/web/src/pages/Landing.tsx +17 -2
  11. package/dist/templates/agentic-app/claude-agent-sdk/web/src/pages/TalkOnMobile.tsx +2 -18
  12. package/dist/templates/agentic-app/claude-agent-sdk/web/src/routes.ts +3 -3
  13. package/dist/templates/agentic-app/claude-agent-sdk/web/src/styles-app.css +22 -24
  14. package/dist/templates/agentic-app/claude-agent-sdk/web/src/vite-env.d.ts +13 -0
  15. package/dist/templates/agentic-app/google-adk/package.json +2 -2
  16. package/dist/templates/agentic-app/google-adk/scripts/bootstrap.mjs +22 -0
  17. package/dist/templates/agentic-app/google-adk/web/package.json +2 -1
  18. package/dist/templates/agentic-app/google-adk/web/src/components/AgentChat.tsx +46 -3
  19. package/dist/templates/agentic-app/google-adk/web/src/components/AppShell.tsx +109 -21
  20. package/dist/templates/agentic-app/google-adk/web/src/components/QrLink.tsx +25 -0
  21. package/dist/templates/agentic-app/google-adk/web/src/main.tsx +2 -3
  22. package/dist/templates/agentic-app/google-adk/web/src/pages/Dashboard.tsx +2 -5
  23. package/dist/templates/agentic-app/google-adk/web/src/pages/Landing.tsx +17 -2
  24. package/dist/templates/agentic-app/google-adk/web/src/pages/TalkOnMobile.tsx +2 -18
  25. package/dist/templates/agentic-app/google-adk/web/src/routes.ts +3 -3
  26. package/dist/templates/agentic-app/google-adk/web/src/styles-app.css +22 -24
  27. package/dist/templates/agentic-app/google-adk/web/src/vite-env.d.ts +13 -0
  28. package/dist/templates/agentic-app/openai-agents-sdk/README.md +6 -4
  29. package/dist/templates/agentic-app/openai-agents-sdk/package.json +3 -3
  30. package/dist/templates/agentic-app/openai-agents-sdk/scripts/bootstrap.mjs +22 -0
  31. package/dist/templates/agentic-app/openai-agents-sdk/web/package.json +2 -1
  32. package/dist/templates/agentic-app/openai-agents-sdk/web/src/components/AgentChat.tsx +46 -3
  33. package/dist/templates/agentic-app/openai-agents-sdk/web/src/components/AppShell.tsx +109 -21
  34. package/dist/templates/agentic-app/openai-agents-sdk/web/src/components/QrLink.tsx +25 -0
  35. package/dist/templates/agentic-app/openai-agents-sdk/web/src/main.tsx +2 -3
  36. package/dist/templates/agentic-app/openai-agents-sdk/web/src/pages/Dashboard.tsx +2 -5
  37. package/dist/templates/agentic-app/openai-agents-sdk/web/src/pages/Landing.tsx +17 -2
  38. package/dist/templates/agentic-app/openai-agents-sdk/web/src/pages/TalkOnMobile.tsx +2 -18
  39. package/dist/templates/agentic-app/openai-agents-sdk/web/src/routes.ts +3 -3
  40. package/dist/templates/agentic-app/openai-agents-sdk/web/src/styles-app.css +22 -24
  41. package/dist/templates/agentic-app/openai-agents-sdk/web/src/vite-env.d.ts +13 -0
  42. package/dist/templates/base/claude-agent-sdk/README.md +6 -4
  43. package/dist/templates/base/claude-agent-sdk/package.json +3 -3
  44. package/dist/templates/base/claude-agent-sdk/scripts/bootstrap.mjs +22 -0
  45. package/dist/templates/base/claude-agent-sdk/web/package.json +3 -1
  46. package/dist/templates/base/claude-agent-sdk/web/src/components/AgentChat.tsx +46 -3
  47. package/dist/templates/base/claude-agent-sdk/web/src/components/QrLink.tsx +25 -0
  48. package/dist/templates/base/claude-agent-sdk/web/src/main.tsx +14 -0
  49. package/dist/templates/base/claude-agent-sdk/web/src/pages/Landing.tsx +17 -2
  50. package/dist/templates/base/claude-agent-sdk/web/src/vite-env.d.ts +13 -0
  51. package/dist/templates/base/google-adk/package.json +2 -2
  52. package/dist/templates/base/google-adk/scripts/bootstrap.mjs +22 -0
  53. package/dist/templates/base/google-adk/web/package.json +3 -1
  54. package/dist/templates/base/google-adk/web/src/components/AgentChat.tsx +46 -3
  55. package/dist/templates/base/google-adk/web/src/components/QrLink.tsx +25 -0
  56. package/dist/templates/base/google-adk/web/src/main.tsx +14 -0
  57. package/dist/templates/base/google-adk/web/src/pages/Landing.tsx +17 -2
  58. package/dist/templates/base/google-adk/web/src/vite-env.d.ts +13 -0
  59. package/dist/templates/base/openai-agents-sdk/README.md +6 -4
  60. package/dist/templates/base/openai-agents-sdk/package.json +3 -3
  61. package/dist/templates/base/openai-agents-sdk/scripts/bootstrap.mjs +22 -0
  62. package/dist/templates/base/openai-agents-sdk/web/package.json +3 -1
  63. package/dist/templates/base/openai-agents-sdk/web/src/components/AgentChat.tsx +46 -3
  64. package/dist/templates/base/openai-agents-sdk/web/src/components/QrLink.tsx +25 -0
  65. package/dist/templates/base/openai-agents-sdk/web/src/main.tsx +14 -0
  66. package/dist/templates/base/openai-agents-sdk/web/src/pages/Landing.tsx +17 -2
  67. package/dist/templates/base/openai-agents-sdk/web/src/vite-env.d.ts +13 -0
  68. package/package.json +3 -3
  69. package/dist/templates/agentic-app/claude-agent-sdk/web/src/pages/AgentCanvas.tsx +0 -14
  70. package/dist/templates/agentic-app/google-adk/web/src/pages/AgentCanvas.tsx +0 -14
  71. package/dist/templates/agentic-app/openai-agents-sdk/web/src/pages/AgentCanvas.tsx +0 -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/ + themes/ — generative-UI config.
19
- │ # `ggui serve` runs against this locally; it's pushed to your
20
- │ # deployed app's guuey-managed ggui instance on `guuey deploy`.
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/` (ggui.json, blueprints, themes) to your
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
  "@anthropic-ai/claude-agent-sdk": "^0.3.199",
15
- "@guuey/config": "0.10.0",
16
- "@guuey/worker": "0.10.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.10.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,7 +9,8 @@
9
9
  "typecheck": "tsc --noEmit"
10
10
  },
11
11
  "dependencies": {
12
- "@guuey/chat": "0.10.0",
12
+ "@guuey/chat": "0.12.0",
13
+ "@guuey/mcp-apps-host": "0.12.0",
13
14
  "oidc-client-ts": "^3.1.0",
14
15
  "qrcode": "^1.5.4",
15
16
  "react": "^19.0.0",
@@ -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
- export function AgentChat({ className, style }: { className?: string; style?: CSSProperties }) {
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={() => getBearerToken()} />
92
+ <GuueyChat {...shared} getAccessToken={getBearerToken} />
50
93
  ) : (
51
- <GuueyChat {...shared} getGuestSecret={() => ensureGuestSecret()} />
94
+ <GuueyChat {...shared} getGuestSecret={ensureGuestSecret} />
52
95
  );
53
96
  }
@@ -1,23 +1,31 @@
1
1
  /**
2
- * The split sidebar (the agentic-app pattern):
2
+ * The CHAT-RAIL shell (guuey#303, founder-confirmed):
3
3
  *
4
4
  * ┌──────────┬────────────────────────┐
5
5
  * │ ☰ Menu 1 │ │
6
- * │ ☰ Menu 2 │ FULLSCREEN AGENT
7
- * │ ☰ Menu 3 │ chat + generative
8
- * ├──────────┤ UI cards on the
9
- * │ ● Agent whole main canvas
10
- * │ 📱 Talk
11
- * │ mobile │ │
6
+ * │ ☰ Menu 2 │ MAIN CANVAS
7
+ * │ ☰ Menu 3 │ your pages — or the
8
+ * ├──────────┤ full generated UI
9
+ * │ AGENT when the agent draws
10
+ * │ RAIL │ one
11
+ * │ (chat) │ │
12
12
  * └──────────┴────────────────────────┘
13
13
  *
14
- * Upper sidebar = your product's menus. Lower sidebar = the agent DOCK.
15
- * Activating the dock swaps the MAIN canvas to the fullscreen agent (the
16
- * generative-UI cards get the whole width) the agent is never a cramped
17
- * pane inside the sidebar. Selecting a menu swaps the canvas back.
14
+ * Upper sidebar = your product's menus. LOWER sidebar = the embed-SDK
15
+ * agent rail the visitor TYPES THERE; generative views collapse to
16
+ * compact CHIPS in the rail (the kit's chips presentation) and the full
17
+ * render takes the MAIN canvas. Chips are the history: clicking one
18
+ * re-selects its render onto the canvas ("just like a web browser's
19
+ * history"); a new render navigates forward automatically; picking a
20
+ * menu swaps the canvas back to your pages.
18
21
  */
22
+ import { useCallback, useRef, useState } from "react";
23
+ import type { GuueyChatHandle } from "@guuey/chat/react";
19
24
  import { NavLink, Outlet, useNavigate } from "react-router-dom";
25
+ import type { PlanViewSummary, ViewRefItem } from "@guuey/chat";
26
+ import { GuueyView } from "@guuey/mcp-apps-host/react";
20
27
  import { appConfig } from "../config";
28
+ import { AgentChat } from "../components/AgentChat";
21
29
  import { currentIdentityMode, logOut } from "../lib/identity";
22
30
  import { oidcConfigured, signOutOidc } from "../lib/oidc";
23
31
 
@@ -25,6 +33,45 @@ export function AppShell() {
25
33
  const navigate = useNavigate();
26
34
  const mode = currentIdentityMode();
27
35
 
36
+ // The rail↔canvas bridge: the kit's view roster (mount material lives
37
+ // here, the rail shows only chips), the selected key, and whether the
38
+ // canvas currently shows a view or the routed pages.
39
+ const [views, setViews] = useState<PlanViewSummary[]>([]);
40
+ // The chat handle carries the kit's view-host wiring (action relay,
41
+ // model-context sink, theme announce) — the canvas mounts with it so a
42
+ // rendered card's Confirm works OUTSIDE the transcript too (guuey#335).
43
+ const [chat, setChat] = useState<GuueyChatHandle | null>(null);
44
+ const [selectedKey, setSelectedKey] = useState<string | undefined>(undefined);
45
+ const [canvasShowsView, setCanvasShowsView] = useState(false);
46
+ const newestKeyRef = useRef<string | undefined>(undefined);
47
+
48
+ const onViewsChange = useCallback((next: PlanViewSummary[]) => {
49
+ setViews(next);
50
+ // Browser-history forward-navigation: a NEW live render takes the
51
+ // canvas. (Reversed find = newest mountable; live entries sit after
52
+ // history in the roster's transcript order.)
53
+ const newest = [...next].reverse().find((v) => v.mount !== null && v.phase !== "expired");
54
+ if (newest !== undefined && newest.key !== newestKeyRef.current) {
55
+ newestKeyRef.current = newest.key;
56
+ setSelectedKey(newest.key);
57
+ setCanvasShowsView(true);
58
+ // The demo-tour hook (guuey#303): step machines outside the app can
59
+ // key on "the agent just drew UI".
60
+ window.dispatchEvent(
61
+ new CustomEvent("demo:render-complete", {
62
+ detail: { key: newest.key, title: newest.title },
63
+ }),
64
+ );
65
+ }
66
+ }, []);
67
+
68
+ const onViewRef = useCallback((item: ViewRefItem) => {
69
+ setSelectedKey(item.key);
70
+ setCanvasShowsView(true);
71
+ }, []);
72
+
73
+ const selected = views.find((v) => v.key === selectedKey && v.mount !== null);
74
+
28
75
  async function handleLogOut() {
29
76
  if (mode === "oidc" && oidcConfigured()) await signOutOidc();
30
77
  logOut();
@@ -40,24 +87,65 @@ export function AppShell() {
40
87
  <span>{appConfig.brand.name}</span>
41
88
  </div>
42
89
  <nav className="sidebar-menus">
43
- <NavLink to="/app" end>
90
+ {/* Picking a menu swaps the canvas back to your pages. */}
91
+ <NavLink to="/app" end onClick={() => setCanvasShowsView(false)}>
44
92
  Dashboard
45
93
  </NavLink>
46
- <NavLink to="/app/reports">Reports</NavLink>
47
- <NavLink to="/app/setup">Setup</NavLink>
48
- </nav>
49
- <nav className="sidebar-dock">
50
- <NavLink to="/app/agent" className="dock-agent">
51
- <span className="dock-dot" /> Agent
94
+ <NavLink to="/app/reports" onClick={() => setCanvasShowsView(false)}>
95
+ Reports
96
+ </NavLink>
97
+ <NavLink to="/app/setup" onClick={() => setCanvasShowsView(false)}>
98
+ Setup
99
+ </NavLink>
100
+ <NavLink to="/app/mobile" onClick={() => setCanvasShowsView(false)}>
101
+ 📱 Talk on mobile
52
102
  </NavLink>
53
- <NavLink to="/app/mobile">📱 Talk on mobile</NavLink>
54
103
  <button type="button" className="dock-logout" onClick={() => void handleLogOut()}>
55
104
  Log out{mode === "guest" ? " (guest)" : ""}
56
105
  </button>
57
106
  </nav>
107
+ <div className="agent-rail" data-tour="agent-rail">
108
+ <AgentChat
109
+ className="rail-chat"
110
+ viewsBridge={{ promotedViewKey: selectedKey, onViewRef, onViewsChange }}
111
+ onReady={setChat}
112
+ />
113
+ </div>
58
114
  </aside>
59
- <main className="canvas">
60
- <Outlet />
115
+ <main className="canvas" data-tour="canvas">
116
+ {canvasShowsView && selected !== undefined && selected.mount !== null ? (
117
+ <div className="canvas-view">
118
+ <header className="canvas-view-bar">
119
+ <span>{selected.title}</span>
120
+ <button type="button" className="btn" onClick={() => setCanvasShowsView(false)}>
121
+ Back to {appConfig.brand.name}
122
+ </button>
123
+ </header>
124
+ {selected.mount.channel !== "locator" ? (
125
+ <GuueyView
126
+ key={selected.key}
127
+ mount={selected.mount}
128
+ title={selected.title}
129
+ className="canvas-view-mount"
130
+ // The kit's slot wiring (guuey#335): action relay +
131
+ // model-context sink + the #302 theme announce, identical
132
+ // to the transcript's own mounts — Confirm inside a
133
+ // canvas-mounted card is a tools/call and needs the relay.
134
+ {...(chat !== null ? chat.viewSlotProps() : {})}
135
+ // Explicit announce kept (same value the kit defaults):
136
+ // the canvas mounts correctly even pre-handle.
137
+ hostContext={{ theme: appConfig.theme.mode }}
138
+ />
139
+ ) : (
140
+ // A locator still resolving — the kit reads it and the next
141
+ // roster emission carries the material (a FAILED read
142
+ // surfaces as the chip's expired state instead).
143
+ <p className="calm canvas-view-loading">Loading card…</p>
144
+ )}
145
+ </div>
146
+ ) : (
147
+ <Outlet />
148
+ )}
61
149
  </main>
62
150
  </div>
63
151
  </div>
@@ -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
+ }
@@ -13,7 +13,6 @@ import { Login } from "./pages/Login";
13
13
  import { Home } from "./pages/Home";
14
14
  import { Dashboard } from "./pages/Dashboard";
15
15
  import { Reports } from "./pages/Reports";
16
- import { AgentCanvas } from "./pages/AgentCanvas";
17
16
  import { TalkOnMobile } from "./pages/TalkOnMobile";
18
17
 
19
18
  document.documentElement.dataset.mode = appConfig.theme.mode;
@@ -30,14 +29,14 @@ const router = createBrowserRouter([
30
29
  ],
31
30
  },
32
31
  {
33
- // The product: split sidebar, fullscreen-swap agent canvas.
32
+ // The product: chat-rail shell the agent rail in the lower
33
+ // sidebar, generated UI on the main canvas (guuey#303).
34
34
  path: "/app",
35
35
  element: <AppShell />,
36
36
  children: [
37
37
  { index: true, element: <Dashboard /> },
38
38
  { path: "reports", element: <Reports /> },
39
39
  { path: "setup", element: <Home /> },
40
- { path: "agent", element: <AgentCanvas /> },
41
40
  { path: "mobile", element: <TalkOnMobile /> },
42
41
  ],
43
42
  },
@@ -12,15 +12,12 @@ export function Dashboard() {
12
12
  <h1>Dashboard</h1>
13
13
  <p className="calm">
14
14
  This is your product's canvas — replace this page with the real thing. The agent lives in
15
- the dock at the bottom of the sidebar; activating it swaps this whole canvas to the
16
- fullscreen agent.
15
+ the RAIL at the bottom of the sidebar: type there, and any UI the agent draws takes this
16
+ whole canvas (the rail's chips are the history — click one to bring its render back).
17
17
  </p>
18
18
  <section className="card">
19
19
  <h2>Getting oriented</h2>
20
20
  <ul>
21
- <li>
22
- <Link to="/app/agent">● Agent</Link> — the fullscreen chat with generative UI
23
- </li>
24
21
  <li>
25
22
  <Link to="/app/mobile">📱 Talk on mobile</Link> — the same agent, on a phone via the
26
23
  guuey portal
@@ -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
@@ -5,23 +5,13 @@
5
5
  * never the chat route directly. Plain URL today; upgrades to a native
6
6
  * deep link when portal applinks land (same path, so the QR stays valid).
7
7
  */
8
- import { useEffect, useRef, useState } from "react";
9
- import QRCode from "qrcode";
10
8
  import { appConfig } from "../config";
9
+ import { QrLink } from "../components/QrLink";
11
10
 
12
11
  export function TalkOnMobile() {
13
- const canvasRef = useRef<HTMLCanvasElement | null>(null);
14
- const [error, setError] = useState<string | null>(null);
15
12
  const link = appConfig.link;
16
13
  const target = link ? `${link.portalUrl}/agent/${link.slug ?? link.appId}` : null;
17
14
 
18
- useEffect(() => {
19
- if (!target || !canvasRef.current) return;
20
- QRCode.toCanvas(canvasRef.current, target, { width: 240, margin: 1 }).catch((err: unknown) => {
21
- setError(err instanceof Error ? err.message : String(err));
22
- });
23
- }, [target]);
24
-
25
15
  return (
26
16
  <div className="page mobile-page">
27
17
  <h1>Talk on mobile</h1>
@@ -31,13 +21,7 @@ export function TalkOnMobile() {
31
21
  Scan with a phone — the same agent, in the guuey portal. (The portal
32
22
  has its own sign-in, so phone conversations are separate threads.)
33
23
  </p>
34
- <canvas ref={canvasRef} className="qr" />
35
- <p>
36
- <a href={target} target="_blank" rel="noreferrer">
37
- {target}
38
- </a>
39
- </p>
40
- {error ? <p className="error">QR render failed: {error}</p> : null}
24
+ <QrLink url={target} size={240} />
41
25
  </>
42
26
  ) : (
43
27
  <p className="calm">
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Route constants shared by the chrome and pages. In the agentic-app
3
- * template the chat is the fullscreen agent canvas inside the app shell,
4
- * and the setup guide lives there too.
3
+ * template the agent RAIL lives inside the app shell (guuey#303's
4
+ * chat-rail layout) — "open the chat" means entering the app.
5
5
  */
6
- export const CHAT_PATH = "/app/agent";
6
+ export const CHAT_PATH = "/app";
7
7
  export const HOME_PATH = "/app/setup";
@@ -14,7 +14,7 @@
14
14
  }
15
15
 
16
16
  .sidebar {
17
- width: 220px;
17
+ width: 320px;
18
18
  flex-shrink: 0;
19
19
  display: flex;
20
20
  flex-direction: column;
@@ -51,42 +51,40 @@
51
51
  font-weight: 600;
52
52
  }
53
53
 
54
- .sidebar-dock {
54
+ .agent-rail {
55
55
  display: flex;
56
56
  flex-direction: column;
57
- gap: 2px;
58
- padding: 8px;
57
+ min-height: 0;
58
+ height: 46vh;
59
59
  border-top: 1px solid var(--line);
60
+ background: var(--bg);
60
61
  }
61
62
 
62
- .sidebar-dock a {
63
- padding: 9px 12px;
64
- border-radius: 8px;
65
- color: var(--fg-soft);
66
- text-decoration: none;
63
+ .rail-chat {
64
+ flex: 1;
65
+ min-height: 0;
67
66
  }
68
67
 
69
- .sidebar-dock a.active {
70
- background: var(--app-accent);
71
- color: #fff;
72
- font-weight: 600;
68
+ .canvas-view {
69
+ flex: 1;
70
+ display: flex;
71
+ flex-direction: column;
72
+ min-height: 0;
73
73
  }
74
74
 
75
- .dock-agent {
75
+ .canvas-view-bar {
76
76
  display: flex;
77
77
  align-items: center;
78
- gap: 8px;
79
- }
80
-
81
- .dock-dot {
82
- width: 8px;
83
- height: 8px;
84
- border-radius: 50%;
85
- background: var(--ok);
78
+ justify-content: space-between;
79
+ gap: 12px;
80
+ padding: 8px 16px;
81
+ border-bottom: 1px solid var(--line);
82
+ font-weight: 600;
86
83
  }
87
84
 
88
- .dock-agent.active .dock-dot {
89
- background: #fff;
85
+ .canvas-view-mount {
86
+ flex: 1;
87
+ min-height: 0;
90
88
  }
91
89
 
92
90
  .dock-logout {
@@ -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
+ }
@@ -16,8 +16,8 @@
16
16
  },
17
17
  "devDependencies": {
18
18
  "@ggui-ai/cli": "0.6.3",
19
- "@guuey/cli": "0.10.0",
20
- "@guuey/config": "0.10.0",
19
+ "@guuey/cli": "0.12.0",
20
+ "@guuey/config": "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,7 +9,8 @@
9
9
  "typecheck": "tsc --noEmit"
10
10
  },
11
11
  "dependencies": {
12
- "@guuey/chat": "0.10.0",
12
+ "@guuey/chat": "0.12.0",
13
+ "@guuey/mcp-apps-host": "0.12.0",
13
14
  "oidc-client-ts": "^3.1.0",
14
15
  "qrcode": "^1.5.4",
15
16
  "react": "^19.0.0",