@guuey/create-agentic-app 0.15.1 → 0.16.1

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 (34) hide show
  1. package/dist/templates/agentic-app/claude-agent-sdk/package.json +3 -3
  2. package/dist/templates/agentic-app/claude-agent-sdk/prompts/system.md +6 -0
  3. package/dist/templates/agentic-app/claude-agent-sdk/web/package.json +3 -2
  4. package/dist/templates/agentic-app/claude-agent-sdk/web/src/components/AgentChat.tsx +5 -1
  5. package/dist/templates/agentic-app/claude-agent-sdk/web/src/components/AppShell.tsx +104 -53
  6. package/dist/templates/agentic-app/claude-agent-sdk/web/src/main.tsx +1 -0
  7. package/dist/templates/agentic-app/claude-agent-sdk/web/src/styles-app.css +15 -8
  8. package/dist/templates/agentic-app/google-adk/package.json +2 -2
  9. package/dist/templates/agentic-app/google-adk/prompts/system.md +6 -0
  10. package/dist/templates/agentic-app/google-adk/web/package.json +3 -2
  11. package/dist/templates/agentic-app/google-adk/web/src/components/AgentChat.tsx +5 -1
  12. package/dist/templates/agentic-app/google-adk/web/src/components/AppShell.tsx +104 -53
  13. package/dist/templates/agentic-app/google-adk/web/src/main.tsx +1 -0
  14. package/dist/templates/agentic-app/google-adk/web/src/styles-app.css +15 -8
  15. package/dist/templates/agentic-app/openai-agents-sdk/package.json +3 -3
  16. package/dist/templates/agentic-app/openai-agents-sdk/prompts/system.md +6 -0
  17. package/dist/templates/agentic-app/openai-agents-sdk/web/package.json +3 -2
  18. package/dist/templates/agentic-app/openai-agents-sdk/web/src/components/AgentChat.tsx +5 -1
  19. package/dist/templates/agentic-app/openai-agents-sdk/web/src/components/AppShell.tsx +104 -53
  20. package/dist/templates/agentic-app/openai-agents-sdk/web/src/main.tsx +1 -0
  21. package/dist/templates/agentic-app/openai-agents-sdk/web/src/styles-app.css +15 -8
  22. package/dist/templates/base/claude-agent-sdk/package.json +3 -3
  23. package/dist/templates/base/claude-agent-sdk/prompts/system.md +6 -0
  24. package/dist/templates/base/claude-agent-sdk/web/package.json +1 -1
  25. package/dist/templates/base/claude-agent-sdk/web/src/components/AgentChat.tsx +5 -1
  26. package/dist/templates/base/google-adk/package.json +2 -2
  27. package/dist/templates/base/google-adk/prompts/system.md +6 -0
  28. package/dist/templates/base/google-adk/web/package.json +1 -1
  29. package/dist/templates/base/google-adk/web/src/components/AgentChat.tsx +5 -1
  30. package/dist/templates/base/openai-agents-sdk/package.json +3 -3
  31. package/dist/templates/base/openai-agents-sdk/prompts/system.md +6 -0
  32. package/dist/templates/base/openai-agents-sdk/web/package.json +1 -1
  33. package/dist/templates/base/openai-agents-sdk/web/src/components/AgentChat.tsx +5 -1
  34. package/package.json +3 -3
@@ -12,12 +12,12 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@anthropic-ai/claude-agent-sdk": "^0.3.199",
15
- "@guuey/config": "0.15.1",
16
- "@guuey/worker": "0.15.1"
15
+ "@guuey/config": "0.16.1",
16
+ "@guuey/worker": "0.16.1"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@ggui-ai/cli": "0.12.0",
20
- "@guuey/cli": "0.15.1",
20
+ "@guuey/cli": "0.16.1",
21
21
  "tsup": "^8.5.0",
22
22
  "tsx": "^4.19.0",
23
23
  "typescript": "^5.8.0"
@@ -7,3 +7,9 @@ loop by default; keep plain prose for one-line answers with nothing to
7
7
  show. After changing todos (create, toggle, delete), render the updated
8
8
  list, so every turn ends on a surface that reflects the current state.
9
9
  Use only the fields the surface's schema declares.
10
+
11
+ An under-specified ask is NEVER answered with a text-only list of
12
+ questions: render the closest useful surface FIRST — the current list,
13
+ the options, a form — and ask your one narrowing question in a short
14
+ line beside it. Clarification happens ON a surface, from the very first
15
+ turn.
@@ -9,8 +9,9 @@
9
9
  "typecheck": "tsc --noEmit"
10
10
  },
11
11
  "dependencies": {
12
- "@guuey/chat": "0.15.1",
13
- "@guuey/mcp-apps-host": "0.15.1",
12
+ "@guuey/agent-layout": "0.16.1",
13
+ "@guuey/chat": "0.16.1",
14
+ "@guuey/mcp-apps-host": "0.16.1",
14
15
  "oidc-client-ts": "^3.1.0",
15
16
  "qrcode": "^1.5.4",
16
17
  "react": "^19.0.0",
@@ -10,7 +10,7 @@
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";
13
+ import type { GuueyChatActivityEvent, GuueyChatHandle } from "@guuey/chat/react";
14
14
  import type { PlanViewSummary, ViewRefItem } from "@guuey/chat";
15
15
  import { agentEndpointUrl, appConfig, historyBaseUrl } from "../config";
16
16
  import { currentIdentityMode, ensureGuestSecret } from "../lib/identity";
@@ -40,12 +40,15 @@ export function AgentChat({
40
40
  style,
41
41
  viewsBridge,
42
42
  onReady,
43
+ onActivity,
43
44
  }: {
44
45
  className?: string;
45
46
  style?: CSSProperties;
46
47
  viewsBridge?: ViewsBridge;
47
48
  /** Receives the chat handle — the AppShell mounts canvas views with `handle.viewSlotProps()`. */
48
49
  onReady?: (handle: GuueyChatHandle) => void;
50
+ /** Turn-lifecycle edges (guuey#403) — the agent-layout bridge's seam. */
51
+ onActivity?: (event: GuueyChatActivityEvent) => void;
49
52
  }) {
50
53
  // The user's CHOSEN mode wins: an explicit "Continue as guest" must never
51
54
  // be shadowed by a cached OIDC session. Only when no choice is recorded
@@ -76,6 +79,7 @@ export function AgentChat({
76
79
  className,
77
80
  style,
78
81
  ...(onReady !== undefined ? { onReady } : {}),
82
+ ...(onActivity !== undefined ? { onActivity } : {}),
79
83
  ...(viewsBridge !== undefined
80
84
  ? {
81
85
  policy: RAIL_POLICY,
@@ -1,5 +1,7 @@
1
1
  /**
2
- * The CHAT-RAIL shell (guuey#303, founder-confirmed):
2
+ * The CHAT-RAIL shell (guuey#303, founder-confirmed), on
3
+ * `@guuey/agent-layout` (guuey#403 — this migration is the lib's
4
+ * acceptance test):
3
5
  *
4
6
  * ┌──────────┬────────────────────────┐
5
7
  * │ ☰ Menu 1 │ │
@@ -18,20 +20,54 @@
18
20
  * re-selects its render onto the canvas ("just like a web browser's
19
21
  * history"); a new render navigates forward automatically; picking a
20
22
  * menu swaps the canvas back to your pages.
23
+ *
24
+ * The layout lib owns the AGENT-MODE physics — the two-tone sidebar, the
25
+ * pane's ground following the user's attention (submit → agent tone; any
26
+ * navigation → app tone, derived from the route change), the working
27
+ * state while the agent has the room, light/dark via the SAME mode you
28
+ * give the chat. The shell wires it once: `navigationKey` from the
29
+ * router, `bindGuueyChat` onto the rail, `agentViewClosed` on the back
30
+ * affordance. Zero per-link wiring.
21
31
  */
22
- import { useCallback, useRef, useState } from "react";
32
+ import { useCallback, useMemo, useRef, useState } from "react";
23
33
  import type { GuueyChatHandle } from "@guuey/chat/react";
24
- import { NavLink, Outlet, useNavigate } from "react-router-dom";
34
+ import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
25
35
  import type { PlanViewSummary, ViewRefItem } from "@guuey/chat";
26
36
  import { GuueyView } from "@guuey/mcp-apps-host/react";
37
+ import {
38
+ ActivePane,
39
+ AgentModeProvider,
40
+ AgentModeShell,
41
+ AgentModeSidebar,
42
+ SidebarPanel,
43
+ bindGuueyChat,
44
+ useAgentMode,
45
+ } from "@guuey/agent-layout/react";
27
46
  import { appConfig } from "../config";
28
47
  import { AgentChat } from "../components/AgentChat";
29
48
  import { currentIdentityMode, logOut } from "../lib/identity";
30
49
  import { oidcConfigured, signOutOidc } from "../lib/oidc";
31
50
 
32
51
  export function AppShell() {
52
+ const location = useLocation();
53
+ return (
54
+ <AgentModeProvider
55
+ mode={appConfig.theme.mode}
56
+ navigationKey={location.pathname}
57
+ identity={<span className="brand-mark">{appConfig.brand.logoText}</span>}
58
+ >
59
+ <ShellBody />
60
+ </AgentModeProvider>
61
+ );
62
+ }
63
+
64
+ function ShellBody() {
33
65
  const navigate = useNavigate();
34
66
  const mode = currentIdentityMode();
67
+ const { dispatch } = useAgentMode();
68
+ // The lib's chat bridge: submit/settle/view-mount flow into the
69
+ // active-panel machine; the shell composes its own roster handling on top.
70
+ const binding = useMemo(() => bindGuueyChat(dispatch), [dispatch]);
35
71
 
36
72
  // The rail↔canvas bridge: the kit's view roster (mount material lives
37
73
  // here, the rail shows only chips), the selected key, and whether the
@@ -45,31 +81,42 @@ export function AppShell() {
45
81
  const [canvasShowsView, setCanvasShowsView] = useState(false);
46
82
  const newestKeyRef = useRef<string | undefined>(undefined);
47
83
 
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
- }, []);
84
+ const onViewsChange = useCallback(
85
+ (next: PlanViewSummary[]) => {
86
+ binding.onViewsChange(next); // the (d) working state clears on a live mount
87
+ setViews(next);
88
+ // Browser-history forward-navigation: a NEW live render takes the
89
+ // canvas. (Reversed find = newest mountable; live entries sit after
90
+ // history in the roster's transcript order.)
91
+ const newest = [...next].reverse().find((v) => v.mount !== null && v.phase !== "expired");
92
+ if (newest !== undefined && newest.key !== newestKeyRef.current) {
93
+ newestKeyRef.current = newest.key;
94
+ setSelectedKey(newest.key);
95
+ setCanvasShowsView(true);
96
+ // The demo-tour hook (guuey#303): step machines outside the app can
97
+ // key on "the agent just drew UI".
98
+ window.dispatchEvent(
99
+ new CustomEvent("demo:render-complete", {
100
+ detail: { key: newest.key, title: newest.title },
101
+ }),
102
+ );
103
+ }
104
+ },
105
+ [binding],
106
+ );
67
107
 
68
108
  const onViewRef = useCallback((item: ViewRefItem) => {
69
109
  setSelectedKey(item.key);
70
110
  setCanvasShowsView(true);
71
111
  }, []);
72
112
 
113
+ const closeView = useCallback(() => {
114
+ setCanvasShowsView(false);
115
+ // The lib rule: closing an agent view returns the tone to the menu
116
+ // UNLESS a turn is still streaming.
117
+ dispatch({ type: "agentViewClosed" });
118
+ }, [dispatch]);
119
+
73
120
  const selected = views.find((v) => v.key === selectedKey && v.mount !== null);
74
121
 
75
122
  async function handleLogOut() {
@@ -80,44 +127,48 @@ export function AppShell() {
80
127
 
81
128
  return (
82
129
  <div className="app-shell">
83
- <div className="app-body">
84
- <aside className="sidebar">
85
- <div className="sidebar-brand">
86
- <span className="brand-mark">{appConfig.brand.logoText}</span>
87
- <span>{appConfig.brand.name}</span>
88
- </div>
89
- <nav className="sidebar-menus">
90
- {/* Picking a menu swaps the canvas back to your pages. */}
91
- <NavLink to="/app" end onClick={() => setCanvasShowsView(false)}>
92
- Dashboard
93
- </NavLink>
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
102
- </NavLink>
103
- <button type="button" className="dock-logout" onClick={() => void handleLogOut()}>
104
- Log out{mode === "guest" ? " (guest)" : ""}
105
- </button>
106
- </nav>
107
- <div className="agent-rail" data-tour="agent-rail">
130
+ <AgentModeShell className="app-body">
131
+ <AgentModeSidebar className="sidebar">
132
+ <SidebarPanel section="app" className="sidebar-top">
133
+ <div className="sidebar-brand">
134
+ <span className="brand-mark">{appConfig.brand.logoText}</span>
135
+ <span>{appConfig.brand.name}</span>
136
+ </div>
137
+ <nav className="sidebar-menus">
138
+ {/* Picking a menu swaps the canvas back to your pages the
139
+ TONE follow is the lib's (route change + capture-phase). */}
140
+ <NavLink to="/app" end onClick={() => setCanvasShowsView(false)}>
141
+ Dashboard
142
+ </NavLink>
143
+ <NavLink to="/app/reports" onClick={() => setCanvasShowsView(false)}>
144
+ Reports
145
+ </NavLink>
146
+ <NavLink to="/app/setup" onClick={() => setCanvasShowsView(false)}>
147
+ Setup
148
+ </NavLink>
149
+ <NavLink to="/app/mobile" onClick={() => setCanvasShowsView(false)}>
150
+ 📱 Talk on mobile
151
+ </NavLink>
152
+ <button type="button" className="dock-logout" onClick={() => void handleLogOut()}>
153
+ Log out{mode === "guest" ? " (guest)" : ""}
154
+ </button>
155
+ </nav>
156
+ </SidebarPanel>
157
+ <SidebarPanel section="agent" className="agent-rail" data-tour="agent-rail">
108
158
  <AgentChat
109
159
  className="rail-chat"
110
160
  viewsBridge={{ promotedViewKey: selectedKey, onViewRef, onViewsChange }}
111
161
  onReady={setChat}
162
+ onActivity={binding.onActivity}
112
163
  />
113
- </div>
114
- </aside>
115
- <main className="canvas" data-tour="canvas">
164
+ </SidebarPanel>
165
+ </AgentModeSidebar>
166
+ <ActivePane className="canvas" data-tour="canvas">
116
167
  {canvasShowsView && selected !== undefined && selected.mount !== null ? (
117
168
  <div className="canvas-view">
118
169
  <header className="canvas-view-bar">
119
170
  <span>{selected.title}</span>
120
- <button type="button" className="btn" onClick={() => setCanvasShowsView(false)}>
171
+ <button type="button" className="btn" onClick={closeView}>
121
172
  Back to {appConfig.brand.name}
122
173
  </button>
123
174
  </header>
@@ -146,8 +197,8 @@ export function AppShell() {
146
197
  ) : (
147
198
  <Outlet />
148
199
  )}
149
- </main>
150
- </div>
200
+ </ActivePane>
201
+ </AgentModeShell>
151
202
  </div>
152
203
  );
153
204
  }
@@ -2,6 +2,7 @@ import { StrictMode } from "react";
2
2
  import { createRoot } from "react-dom/client";
3
3
  import { createBrowserRouter, RouterProvider } from "react-router-dom";
4
4
  import "@guuey/chat/styles.css";
5
+ import "@guuey/agent-layout/styles.css";
5
6
  import "./styles.css";
6
7
  import "./styles-app.css";
7
8
  import { appConfig } from "./config";
@@ -7,19 +7,26 @@
7
7
  flex-direction: column;
8
8
  }
9
9
 
10
+ /* The lib's AgentModeShell grid carries this class; the app states its own
11
+ column width (app-tier override of the lib's minmax default). Display
12
+ comes from the lib sheet. */
10
13
  .app-body {
11
14
  flex: 1;
12
- display: flex;
13
15
  min-height: 0;
16
+ grid-template-columns: 320px 1fr;
14
17
  }
15
18
 
19
+ /* Tones come from @guuey/agent-layout (the two-panel pair — light AND
20
+ dark ride the lib's mode-aware tokens); the app keeps its structure. */
16
21
  .sidebar {
17
- width: 320px;
18
- flex-shrink: 0;
22
+ border-right: 1px solid var(--line);
23
+ }
24
+
25
+ .sidebar-top {
26
+ flex: 1;
19
27
  display: flex;
20
28
  flex-direction: column;
21
- border-right: 1px solid var(--line);
22
- background: var(--bg-soft);
29
+ min-height: 0;
23
30
  }
24
31
 
25
32
  .sidebar-brand {
@@ -52,12 +59,12 @@
52
59
  }
53
60
 
54
61
  .agent-rail {
55
- display: flex;
56
- flex-direction: column;
62
+ /* Explicit: the app's fixed rail height wins over the lib's flex:1
63
+ default (deterministic, not load-order luck). */
64
+ flex: 0 0 auto;
57
65
  min-height: 0;
58
66
  height: 46vh;
59
67
  border-top: 1px solid var(--line);
60
- background: var(--bg);
61
68
  }
62
69
 
63
70
  .rail-chat {
@@ -16,8 +16,8 @@
16
16
  },
17
17
  "devDependencies": {
18
18
  "@ggui-ai/cli": "0.12.0",
19
- "@guuey/cli": "0.15.1",
20
- "@guuey/config": "0.15.1",
19
+ "@guuey/cli": "0.16.1",
20
+ "@guuey/config": "0.16.1",
21
21
  "tsup": "^8.5.0",
22
22
  "tsx": "^4.19.0",
23
23
  "typescript": "^5.8.0"
@@ -7,3 +7,9 @@ loop by default; keep plain prose for one-line answers with nothing to
7
7
  show. After changing todos (create, toggle, delete), render the updated
8
8
  list, so every turn ends on a surface that reflects the current state.
9
9
  Use only the fields the surface's schema declares.
10
+
11
+ An under-specified ask is NEVER answered with a text-only list of
12
+ questions: render the closest useful surface FIRST — the current list,
13
+ the options, a form — and ask your one narrowing question in a short
14
+ line beside it. Clarification happens ON a surface, from the very first
15
+ turn.
@@ -9,8 +9,9 @@
9
9
  "typecheck": "tsc --noEmit"
10
10
  },
11
11
  "dependencies": {
12
- "@guuey/chat": "0.15.1",
13
- "@guuey/mcp-apps-host": "0.15.1",
12
+ "@guuey/agent-layout": "0.16.1",
13
+ "@guuey/chat": "0.16.1",
14
+ "@guuey/mcp-apps-host": "0.16.1",
14
15
  "oidc-client-ts": "^3.1.0",
15
16
  "qrcode": "^1.5.4",
16
17
  "react": "^19.0.0",
@@ -10,7 +10,7 @@
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";
13
+ import type { GuueyChatActivityEvent, GuueyChatHandle } from "@guuey/chat/react";
14
14
  import type { PlanViewSummary, ViewRefItem } from "@guuey/chat";
15
15
  import { agentEndpointUrl, appConfig, historyBaseUrl } from "../config";
16
16
  import { currentIdentityMode, ensureGuestSecret } from "../lib/identity";
@@ -40,12 +40,15 @@ export function AgentChat({
40
40
  style,
41
41
  viewsBridge,
42
42
  onReady,
43
+ onActivity,
43
44
  }: {
44
45
  className?: string;
45
46
  style?: CSSProperties;
46
47
  viewsBridge?: ViewsBridge;
47
48
  /** Receives the chat handle — the AppShell mounts canvas views with `handle.viewSlotProps()`. */
48
49
  onReady?: (handle: GuueyChatHandle) => void;
50
+ /** Turn-lifecycle edges (guuey#403) — the agent-layout bridge's seam. */
51
+ onActivity?: (event: GuueyChatActivityEvent) => void;
49
52
  }) {
50
53
  // The user's CHOSEN mode wins: an explicit "Continue as guest" must never
51
54
  // be shadowed by a cached OIDC session. Only when no choice is recorded
@@ -76,6 +79,7 @@ export function AgentChat({
76
79
  className,
77
80
  style,
78
81
  ...(onReady !== undefined ? { onReady } : {}),
82
+ ...(onActivity !== undefined ? { onActivity } : {}),
79
83
  ...(viewsBridge !== undefined
80
84
  ? {
81
85
  policy: RAIL_POLICY,
@@ -1,5 +1,7 @@
1
1
  /**
2
- * The CHAT-RAIL shell (guuey#303, founder-confirmed):
2
+ * The CHAT-RAIL shell (guuey#303, founder-confirmed), on
3
+ * `@guuey/agent-layout` (guuey#403 — this migration is the lib's
4
+ * acceptance test):
3
5
  *
4
6
  * ┌──────────┬────────────────────────┐
5
7
  * │ ☰ Menu 1 │ │
@@ -18,20 +20,54 @@
18
20
  * re-selects its render onto the canvas ("just like a web browser's
19
21
  * history"); a new render navigates forward automatically; picking a
20
22
  * menu swaps the canvas back to your pages.
23
+ *
24
+ * The layout lib owns the AGENT-MODE physics — the two-tone sidebar, the
25
+ * pane's ground following the user's attention (submit → agent tone; any
26
+ * navigation → app tone, derived from the route change), the working
27
+ * state while the agent has the room, light/dark via the SAME mode you
28
+ * give the chat. The shell wires it once: `navigationKey` from the
29
+ * router, `bindGuueyChat` onto the rail, `agentViewClosed` on the back
30
+ * affordance. Zero per-link wiring.
21
31
  */
22
- import { useCallback, useRef, useState } from "react";
32
+ import { useCallback, useMemo, useRef, useState } from "react";
23
33
  import type { GuueyChatHandle } from "@guuey/chat/react";
24
- import { NavLink, Outlet, useNavigate } from "react-router-dom";
34
+ import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
25
35
  import type { PlanViewSummary, ViewRefItem } from "@guuey/chat";
26
36
  import { GuueyView } from "@guuey/mcp-apps-host/react";
37
+ import {
38
+ ActivePane,
39
+ AgentModeProvider,
40
+ AgentModeShell,
41
+ AgentModeSidebar,
42
+ SidebarPanel,
43
+ bindGuueyChat,
44
+ useAgentMode,
45
+ } from "@guuey/agent-layout/react";
27
46
  import { appConfig } from "../config";
28
47
  import { AgentChat } from "../components/AgentChat";
29
48
  import { currentIdentityMode, logOut } from "../lib/identity";
30
49
  import { oidcConfigured, signOutOidc } from "../lib/oidc";
31
50
 
32
51
  export function AppShell() {
52
+ const location = useLocation();
53
+ return (
54
+ <AgentModeProvider
55
+ mode={appConfig.theme.mode}
56
+ navigationKey={location.pathname}
57
+ identity={<span className="brand-mark">{appConfig.brand.logoText}</span>}
58
+ >
59
+ <ShellBody />
60
+ </AgentModeProvider>
61
+ );
62
+ }
63
+
64
+ function ShellBody() {
33
65
  const navigate = useNavigate();
34
66
  const mode = currentIdentityMode();
67
+ const { dispatch } = useAgentMode();
68
+ // The lib's chat bridge: submit/settle/view-mount flow into the
69
+ // active-panel machine; the shell composes its own roster handling on top.
70
+ const binding = useMemo(() => bindGuueyChat(dispatch), [dispatch]);
35
71
 
36
72
  // The rail↔canvas bridge: the kit's view roster (mount material lives
37
73
  // here, the rail shows only chips), the selected key, and whether the
@@ -45,31 +81,42 @@ export function AppShell() {
45
81
  const [canvasShowsView, setCanvasShowsView] = useState(false);
46
82
  const newestKeyRef = useRef<string | undefined>(undefined);
47
83
 
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
- }, []);
84
+ const onViewsChange = useCallback(
85
+ (next: PlanViewSummary[]) => {
86
+ binding.onViewsChange(next); // the (d) working state clears on a live mount
87
+ setViews(next);
88
+ // Browser-history forward-navigation: a NEW live render takes the
89
+ // canvas. (Reversed find = newest mountable; live entries sit after
90
+ // history in the roster's transcript order.)
91
+ const newest = [...next].reverse().find((v) => v.mount !== null && v.phase !== "expired");
92
+ if (newest !== undefined && newest.key !== newestKeyRef.current) {
93
+ newestKeyRef.current = newest.key;
94
+ setSelectedKey(newest.key);
95
+ setCanvasShowsView(true);
96
+ // The demo-tour hook (guuey#303): step machines outside the app can
97
+ // key on "the agent just drew UI".
98
+ window.dispatchEvent(
99
+ new CustomEvent("demo:render-complete", {
100
+ detail: { key: newest.key, title: newest.title },
101
+ }),
102
+ );
103
+ }
104
+ },
105
+ [binding],
106
+ );
67
107
 
68
108
  const onViewRef = useCallback((item: ViewRefItem) => {
69
109
  setSelectedKey(item.key);
70
110
  setCanvasShowsView(true);
71
111
  }, []);
72
112
 
113
+ const closeView = useCallback(() => {
114
+ setCanvasShowsView(false);
115
+ // The lib rule: closing an agent view returns the tone to the menu
116
+ // UNLESS a turn is still streaming.
117
+ dispatch({ type: "agentViewClosed" });
118
+ }, [dispatch]);
119
+
73
120
  const selected = views.find((v) => v.key === selectedKey && v.mount !== null);
74
121
 
75
122
  async function handleLogOut() {
@@ -80,44 +127,48 @@ export function AppShell() {
80
127
 
81
128
  return (
82
129
  <div className="app-shell">
83
- <div className="app-body">
84
- <aside className="sidebar">
85
- <div className="sidebar-brand">
86
- <span className="brand-mark">{appConfig.brand.logoText}</span>
87
- <span>{appConfig.brand.name}</span>
88
- </div>
89
- <nav className="sidebar-menus">
90
- {/* Picking a menu swaps the canvas back to your pages. */}
91
- <NavLink to="/app" end onClick={() => setCanvasShowsView(false)}>
92
- Dashboard
93
- </NavLink>
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
102
- </NavLink>
103
- <button type="button" className="dock-logout" onClick={() => void handleLogOut()}>
104
- Log out{mode === "guest" ? " (guest)" : ""}
105
- </button>
106
- </nav>
107
- <div className="agent-rail" data-tour="agent-rail">
130
+ <AgentModeShell className="app-body">
131
+ <AgentModeSidebar className="sidebar">
132
+ <SidebarPanel section="app" className="sidebar-top">
133
+ <div className="sidebar-brand">
134
+ <span className="brand-mark">{appConfig.brand.logoText}</span>
135
+ <span>{appConfig.brand.name}</span>
136
+ </div>
137
+ <nav className="sidebar-menus">
138
+ {/* Picking a menu swaps the canvas back to your pages the
139
+ TONE follow is the lib's (route change + capture-phase). */}
140
+ <NavLink to="/app" end onClick={() => setCanvasShowsView(false)}>
141
+ Dashboard
142
+ </NavLink>
143
+ <NavLink to="/app/reports" onClick={() => setCanvasShowsView(false)}>
144
+ Reports
145
+ </NavLink>
146
+ <NavLink to="/app/setup" onClick={() => setCanvasShowsView(false)}>
147
+ Setup
148
+ </NavLink>
149
+ <NavLink to="/app/mobile" onClick={() => setCanvasShowsView(false)}>
150
+ 📱 Talk on mobile
151
+ </NavLink>
152
+ <button type="button" className="dock-logout" onClick={() => void handleLogOut()}>
153
+ Log out{mode === "guest" ? " (guest)" : ""}
154
+ </button>
155
+ </nav>
156
+ </SidebarPanel>
157
+ <SidebarPanel section="agent" className="agent-rail" data-tour="agent-rail">
108
158
  <AgentChat
109
159
  className="rail-chat"
110
160
  viewsBridge={{ promotedViewKey: selectedKey, onViewRef, onViewsChange }}
111
161
  onReady={setChat}
162
+ onActivity={binding.onActivity}
112
163
  />
113
- </div>
114
- </aside>
115
- <main className="canvas" data-tour="canvas">
164
+ </SidebarPanel>
165
+ </AgentModeSidebar>
166
+ <ActivePane className="canvas" data-tour="canvas">
116
167
  {canvasShowsView && selected !== undefined && selected.mount !== null ? (
117
168
  <div className="canvas-view">
118
169
  <header className="canvas-view-bar">
119
170
  <span>{selected.title}</span>
120
- <button type="button" className="btn" onClick={() => setCanvasShowsView(false)}>
171
+ <button type="button" className="btn" onClick={closeView}>
121
172
  Back to {appConfig.brand.name}
122
173
  </button>
123
174
  </header>
@@ -146,8 +197,8 @@ export function AppShell() {
146
197
  ) : (
147
198
  <Outlet />
148
199
  )}
149
- </main>
150
- </div>
200
+ </ActivePane>
201
+ </AgentModeShell>
151
202
  </div>
152
203
  );
153
204
  }
@@ -2,6 +2,7 @@ import { StrictMode } from "react";
2
2
  import { createRoot } from "react-dom/client";
3
3
  import { createBrowserRouter, RouterProvider } from "react-router-dom";
4
4
  import "@guuey/chat/styles.css";
5
+ import "@guuey/agent-layout/styles.css";
5
6
  import "./styles.css";
6
7
  import "./styles-app.css";
7
8
  import { appConfig } from "./config";