@fluxy-chat/create-fluxy-chat 0.5.3 → 0.5.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.4] - 2026-08-19
4
+
5
+ ### Fixed
6
+
7
+ - Add `zustand` to `full` template dependencies so `pnpm install` gets everything without manual intervention.
8
+ - Fix `FluxyRealtimeProvider` usage: pass `workerUrl` + `authTokenProvider` instead of non-existent `client` prop (fixes "disconnected" state and message sending).
9
+ - Restyle full template UI to match FluxyChat dark theme (indigo/emerald palette, proper message bubbles, live/disconnected badge).
10
+
11
+ ## [0.5.5] - 2026-08-19
12
+
13
+ ### Fixed
14
+
15
+ - Hosted `full` template now provisions a user project + assistant room via Clerk (no more public playground session).
16
+ - Hosted `full` template uses the same chat UI primitives as FluxyChat (message bubbles/composer/status) to match dashboard/rooms/onboarding styling.
17
+ - Remove fuorvianti "dash" UI artifacts and wire message sending through the same chat path as the dashboard.
18
+
3
19
  ## [0.5.3] - 2026-08-19
4
20
 
5
21
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluxy-chat/create-fluxy-chat",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Scaffold a new FluxyChat bot project with a single command",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,66 +1,47 @@
1
- # FluxyChat full stack starter
1
+ # FluxyChat full stack starter
2
2
 
3
- Chat + AI agent + tool calls in one Vite app. Provisions against a local FluxyChat worker in ~60 seconds.
3
+ Chat + AI agent in a Vite app. Hosted mode signs you in with Clerk (same as the console), then creates your project and assistant room.
4
4
 
5
5
  ## Quick start
6
6
 
7
- **Hosted (no wrangler):**
7
+ **Hosted (Clerk, no wrangler):**
8
8
 
9
9
  ```bash
10
- npx @fluxy-chat/create-fluxy-chat my-app --mode hosted -y
10
+ npx @fluxy-chat/create-fluxy-chat@latest my-app --mode hosted -y
11
11
  cd my-app
12
12
  pnpm install
13
13
  pnpm setup:hosted
14
14
  pnpm dev
15
15
  ```
16
16
 
17
- **Local (full control):**
17
+ The app opens on localhost. Click Continue with FluxyChat, sign in, then you land in your own room (not the public playground).
18
+
19
+ **Local worker:**
18
20
 
19
21
  ```bash
20
- # Terminal 1 — monorepo worker
21
22
  pnpm --filter @fluxy-chat/worker dev
22
-
23
- # Terminal 2 — new project
24
- npx create-fluxy-chat my-app --full -y
23
+ npx @fluxy-chat/create-fluxy-chat@latest my-app --full -y
25
24
  cd my-app
26
25
  pnpm install
27
26
  pnpm setup
28
27
  pnpm dev
29
28
  ```
30
29
 
31
- Optional: `pnpm doctor` checks `.env`, worker health, room access.
32
-
33
- ## What you get
34
-
35
- - Realtime chat (`useChat` + WebSocket)
36
- - `@assistant` invoke with tool call thread in the UI
37
- - Sidebar with project/room/agent metadata
38
- - Link to operator console
39
-
40
30
  ## Scripts
41
31
 
42
32
  | Command | Description |
43
33
  |---------|-------------|
44
- | `pnpm setup:hosted` | Guest JWT via `GET /demo/session` on api.fluxychat.com |
45
- | `pnpm setup:local` | Local `/dev/provision` (same as `pnpm setup`) |
46
- | `pnpm doctor` | Health check: .env, worker, room |
47
- | `pnpm dev` | Start Vite; auto-start worker if FluxyChat monorepo is detected nearby |
48
- | `pnpm dev:app` | Vite only |
34
+ | `pnpm setup:hosted` | Writes worker + console URLs. Auth happens in the browser via Clerk. |
35
+ | `pnpm setup:local` | Local `/dev/provision` |
36
+ | `pnpm doctor` | Health check |
37
+ | `pnpm dev` | Start Vite |
49
38
 
50
39
  ## Environment
51
40
 
52
- See `.env.example`. Generated by `pnpm setup`:
53
-
54
- - `VITE_FLUXYCHAT_WORKER_URL` — default `http://127.0.0.1:8787`
55
- - `VITE_FLUXYCHAT_MEMBER_JWT` — member token with admin roles
56
- - `VITE_FLUXYCHAT_ROOM_ID` — `{projectId}-general`
57
- - `VITE_FLUXYCHAT_AGENT_ID` — built-in `@assistant`
58
-
59
- ## Hosted credentials
41
+ `.env` from `pnpm setup:hosted` only needs worker and console URLs. Member JWT, room, and agent come from Clerk after sign in.
60
42
 
61
- Use [fluxychat.com/onboarding](https://fluxychat.com/onboarding) and copy values into `.env` manually.
43
+ Open console: [fluxychat.com/onboarding](https://fluxychat.com/onboarding)
62
44
 
63
45
  ## Learn more
64
46
 
65
- - [One-click product roadmap](https://github.com/AlessandroFare/fluxychat/blob/main/docs/ONE-CLICK-PRODUCT-ROADMAP.md)
66
47
  - [Docs](https://docs.fluxychat.com)
@@ -4,6 +4,9 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>FluxyChat</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;650&display=swap" rel="stylesheet" />
7
10
  </head>
8
11
  <body>
9
12
  <div id="root"></div>
@@ -16,13 +16,23 @@
16
16
  "dependencies": {
17
17
  "@fluxy-chat/react": "^0.1.1",
18
18
  "@fluxy-chat/sdk": "^0.6.2",
19
+ "@fluxy-chat/ui": "^0.1.2",
20
+ "class-variance-authority": "^0.7.1",
21
+ "clsx": "^2.1.1",
22
+ "lucide-react": "^0.511.0",
23
+ "radix-ui": "^1.4.3",
19
24
  "react": "^19.0.0",
20
- "react-dom": "^19.0.0"
25
+ "react-dom": "^19.0.0",
26
+ "tailwind-merge": "^3.0.0",
27
+ "zustand": "^5.0.14"
21
28
  },
22
29
  "devDependencies": {
23
30
  "@types/react": "^19.0.0",
24
31
  "@types/react-dom": "^19.0.0",
25
32
  "@vitejs/plugin-react": "^4.3.0",
33
+ "autoprefixer": "^10.4.20",
34
+ "postcss": "^8.5.0",
35
+ "tailwindcss": "^3.4.17",
26
36
  "typescript": "^5.6.0",
27
37
  "vite": "^6.0.0"
28
38
  }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
@@ -71,8 +71,11 @@ async function main() {
71
71
  if (!workerUrl) fail("VITE_FLUXYCHAT_WORKER_URL unset");
72
72
  else pass(`worker URL: ${workerUrl}`);
73
73
 
74
- if (!jwt) fail("VITE_FLUXYCHAT_MEMBER_JWT unset");
75
- else pass(`JWT present (${jwt.length} chars)`);
74
+ const hosted = (env.VITE_FLUXYCHAT_CONSOLE_URL || "").includes("fluxychat.com");
75
+ if (!jwt) {
76
+ if (hosted) warn("VITE_FLUXYCHAT_MEMBER_JWT unset — sign in from the app (Clerk)");
77
+ else fail("VITE_FLUXYCHAT_MEMBER_JWT unset");
78
+ } else pass(`JWT present (${jwt.length} chars)`);
76
79
 
77
80
  if (!roomId) warn("VITE_FLUXYCHAT_ROOM_ID unset");
78
81
  else pass(`room: ${roomId}`);
@@ -189,49 +189,32 @@ async function setupHosted() {
189
189
 
190
190
  console.log(dim(` mode: hosted · worker: ${workerUrl}`));
191
191
 
192
- const cliHeaders = { Origin: consoleUrl };
193
-
194
- const status = await getJson(workerUrl, "/demo/status", cliHeaders).catch(() => null);
195
- if (!status?.ready) {
196
- fail(
197
- `Public demo not available at ${workerUrl}\n` +
198
- " Try: pnpm setup -- --mode local (with local worker)\n" +
199
- " Or: https://fluxychat.com/demo",
200
- );
201
- }
202
- ok("public demo ready");
203
-
204
- const session = await getJson(workerUrl, "/demo/session", cliHeaders);
205
- if (!session?.token || !session?.roomId) {
206
- fail("/demo/session did not return token + roomId");
207
- }
208
- ok(`guest session · room ${session.roomId}`);
209
-
210
192
  writeEnv({
211
193
  workerUrl,
212
- token: session.token,
213
- roomId: session.roomId,
214
- agentId: session.agentId ?? "",
215
- agentHandle: session.agentHandle ?? "@assistant",
216
- projectId: "hosted-demo",
217
- userId: session.userId ?? "demo-guest",
194
+ token: "",
195
+ roomId: "",
196
+ agentId: "",
197
+ agentHandle: "@assistant",
198
+ projectId: "",
199
+ userId: "",
218
200
  consoleUrl,
219
201
  });
220
202
 
221
203
  writeMeta({
222
204
  mode: "hosted",
223
205
  workerUrl,
224
- projectId: "hosted-demo",
225
- roomId: session.roomId,
226
- agentId: session.agentId ?? null,
227
- agentHandle: session.agentHandle ?? "@assistant",
228
- expiresIn: session.expiresIn ?? null,
206
+ projectId: null,
207
+ roomId: null,
208
+ agentId: null,
209
+ agentHandle: "@assistant",
229
210
  setupAt: new Date().toISOString(),
211
+ auth: "clerk",
230
212
  });
231
213
 
214
+ ok("wrote hosted endpoints (sign in with Clerk on pnpm dev)");
232
215
  console.log(dim(" Run: pnpm dev"));
233
- console.log(dim(` Keep this project: ${consoleUrl}/onboarding?from=cli`));
234
- console.log(dim(" Paste your .env in the console to continue onboarding."));
216
+ console.log(dim(" Sign in with Clerk. We create your project and assistant room."));
217
+ console.log(dim(` Console: ${consoleUrl}/onboarding`));
235
218
  }
236
219
 
237
220
  async function setupLocal() {
@@ -1,221 +1,200 @@
1
1
  import { useMemo, useState } from "react";
2
- import { FluxyChatClient } from "@fluxy-chat/sdk";
3
2
  import { FluxyRealtimeProvider, useChat } from "@fluxy-chat/react";
3
+ import { ChatWindow } from "@fluxy-chat/ui";
4
+ import { loadCliSession, type CliSession } from "./session";
5
+
6
+ const envWorkerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim() ?? "";
7
+ const envJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim() ?? "";
8
+ const envRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() ?? "";
9
+ const envAgentId = import.meta.env.VITE_FLUXYCHAT_AGENT_ID?.trim() ?? "";
10
+ const envAgentHandle = import.meta.env.VITE_FLUXYCHAT_AGENT_HANDLE?.trim() || "@assistant";
11
+ const envProjectId = import.meta.env.VITE_FLUXYCHAT_PROJECT_ID?.trim() ?? "";
12
+ const envUserId = import.meta.env.VITE_FLUXYCHAT_USER_ID?.trim() ?? "";
13
+ const consoleUrl = import.meta.env.VITE_FLUXYCHAT_CONSOLE_URL?.trim() || "https://fluxychat.com";
14
+
15
+ const SUGGESTED_PROMPTS = [
16
+ "Tell me about FluxyChat features",
17
+ "What can this assistant do?",
18
+ ];
19
+
20
+ function sessionFromEnv(): CliSession | null {
21
+ if (!envWorkerUrl || !envJwt || !envRoomId) return null;
22
+ return {
23
+ workerUrl: envWorkerUrl,
24
+ memberJwt: envJwt,
25
+ roomId: envRoomId,
26
+ agentId: envAgentId,
27
+ agentHandle: envAgentHandle,
28
+ projectId: envProjectId,
29
+ userId: envUserId || "demo-user",
30
+ };
31
+ }
4
32
 
5
- const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim();
6
- const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim();
7
- const roomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "general";
8
- const agentId = import.meta.env.VITE_FLUXYCHAT_AGENT_ID?.trim() || "";
9
- const agentHandle = import.meta.env.VITE_FLUXYCHAT_AGENT_HANDLE?.trim() || "@assistant";
10
- const projectId = import.meta.env.VITE_FLUXYCHAT_PROJECT_ID?.trim() || "";
11
- const consoleUrl = import.meta.env.VITE_FLUXYCHAT_CONSOLE_URL?.trim() || "http://localhost:3000";
12
- const memberUserId =
13
- import.meta.env.VITE_FLUXYCHAT_USER_ID?.trim() || "demo-user";
33
+ function SignInGate() {
34
+ const returnTo = typeof window !== "undefined" ? window.location.origin : "http://localhost:5173";
35
+ const href = `${consoleUrl.replace(/\/$/, "")}/cli-auth?redirect_uri=${encodeURIComponent(returnTo)}`;
14
36
 
15
- function ChatRoom() {
16
- const {
17
- messages,
18
- sendMessage,
19
- invokeAgent,
20
- connectionState,
21
- agentTyping,
22
- toolThreadEvents,
23
- lastAgentRun,
24
- } = useChat({
25
- roomId,
26
- agentId: agentId || undefined,
27
- markReadLatest: true,
28
- });
37
+ return (
38
+ <main className="app-shell">
39
+ <header className="topbar">
40
+ <div className="brand">
41
+ <img src={`${consoleUrl.replace(/\/$/, "")}/fluxychat-icon.svg`} alt="" width={28} height={28} />
42
+ <span>FluxyChat</span>
43
+ </div>
44
+ </header>
45
+ <div className="signin-card">
46
+ <p className="eyebrow">Local starter</p>
47
+ <h1>Sign in to open your room</h1>
48
+ <p>
49
+ Use the same Clerk account as the console. We create your project and
50
+ assistant room, then you chat here.
51
+ </p>
52
+ <a className="btn-primary" href={href}>
53
+ Continue with FluxyChat
54
+ </a>
55
+ <p className="fine-print">
56
+ Opens {consoleUrl.replace(/^https?:\/\//, "")} for sign in, then returns to this app.
57
+ </p>
58
+ </div>
59
+ </main>
60
+ );
61
+ }
29
62
 
30
- const [draft, setDraft] = useState("");
31
- const [invokeError, setInvokeError] = useState<string | null>(null);
63
+ function ChatRoom({ session }: { session: CliSession }) {
64
+ const { messages, sendMessage, invokeAgent, connectionState, agentTyping, typingUsers, online } =
65
+ useChat({
66
+ roomId: session.roomId,
67
+ agentId: session.agentId || undefined,
68
+ markReadLatest: true,
69
+ });
32
70
 
33
- async function handleSendMessage(text: string) {
34
- setInvokeError(null);
35
- await sendMessage(text);
36
- }
71
+ const [error, setError] = useState<string | null>(null);
72
+ const connected = connectionState.status === "connected";
37
73
 
38
- async function handleInvokeAgent(text: string) {
39
- if (!agentId) {
40
- setInvokeError("Set VITE_FLUXYCHAT_AGENT_ID in .env (run pnpm setup).");
41
- return;
42
- }
43
- setInvokeError(null);
74
+ async function onSend(content: string) {
75
+ setError(null);
76
+ const mentionsAgent = content.toLowerCase().includes(
77
+ (session.agentHandle || "@assistant").replace(/^@/, "").toLowerCase(),
78
+ );
44
79
  try {
45
- await invokeAgent(text, { agentId });
80
+ if (mentionsAgent && session.agentId) {
81
+ await invokeAgent(content, { agentId: session.agentId });
82
+ } else {
83
+ await sendMessage(content);
84
+ }
46
85
  } catch (err) {
47
- setInvokeError(err instanceof Error ? err.message : "Agent invoke failed");
86
+ setError(err instanceof Error ? err.message : "Failed to send");
48
87
  }
49
88
  }
50
89
 
51
90
  return (
52
- <section className="chat-panel">
53
- <header className="chat-header">
54
- <strong>{roomId}</strong>
55
- <span className="status">{connectionState.status}</span>
56
- </header>
57
-
58
- <ul className="messages">
59
- {messages.map((m) => {
60
- const isSelf = m.userId === memberUserId || m.userId === "first-message-user";
61
- const isAgent = m.userId?.includes("agent") || m.userId === "assistant";
62
- return (
63
- <li
64
- key={m.id ?? `${m.createdAt}-${m.userId}`}
65
- className={`message${isSelf ? " self" : ""}${isAgent ? " agent" : ""}`}
66
- >
67
- <span className="author">{m.userId}</span>
68
- <span>{m.content}</span>
69
- </li>
70
- );
71
- })}
72
- </ul>
73
-
74
- {agentTyping ? <p className="typing">{agentHandle} is thinking…</p> : null}
75
-
76
- {(toolThreadEvents.length > 0 || lastAgentRun?.toolCalls?.length) ? (
77
- <div className="tools">
78
- <h3>Agent tools</h3>
79
- <ul>
80
- {toolThreadEvents.map((ev) => (
81
- <li key={ev.key}>
82
- {String(ev.kind ?? "tool")}: {String(ev.title ?? ev.toolName ?? ev.key)}
83
- </li>
84
- ))}
85
- {(lastAgentRun?.toolCalls ?? []).map((tc) => (
86
- <li key={tc.id}>
87
- {tc.name}: {tc.status ?? "done"}
88
- </li>
89
- ))}
90
- </ul>
91
- </div>
92
- ) : null}
91
+ <div className="chat-column">
92
+ <div className="status-row">
93
+ <span className="inline-flex items-center gap-2">
94
+ <span className={`status-dot ${connected ? "on" : "off"}`} />
95
+ <span className="font-medium text-foreground">
96
+ {connected ? "Connected" : connectionState.status}
97
+ </span>
98
+ <span className="text-muted-foreground">· {session.roomId}</span>
99
+ </span>
100
+ </div>
93
101
 
94
- {invokeError ? <p className="hint" style={{ color: "#b91c1c", padding: "0 1rem" }}>{invokeError}</p> : null}
102
+ <div className="chat-frame">
103
+ <ChatWindow
104
+ messages={messages}
105
+ online={online ?? 0}
106
+ typingUsers={typingUsers ?? {}}
107
+ onSend={(content) => {
108
+ void onSend(content);
109
+ }}
110
+ agentTyping={Boolean(agentTyping)}
111
+ agentTypingLabel={session.agentHandle || "@assistant"}
112
+ mentionSuggestions={[
113
+ {
114
+ handle: (session.agentHandle || "@assistant").replace(/^@/, ""),
115
+ label: session.agentHandle || "@assistant",
116
+ agentId: session.agentId,
117
+ },
118
+ ]}
119
+ />
120
+ </div>
95
121
 
96
- <form
97
- className="composer"
98
- onSubmit={(e) => {
99
- e.preventDefault();
100
- const text = draft.trim();
101
- if (!text) return;
102
- void handleSendMessage(text);
103
- setDraft("");
104
- }}
105
- >
106
- <div className="composer-row">
107
- <input
108
- value={draft}
109
- onChange={(e) => setDraft(e.target.value)}
110
- placeholder="Message or ask @assistant…"
111
- aria-label="Message"
112
- />
113
- <button type="submit">Send</button>
122
+ <div className="prompts">
123
+ {SUGGESTED_PROMPTS.map((prompt) => (
114
124
  <button
125
+ key={prompt}
115
126
  type="button"
116
- className="secondary"
117
- disabled={!draft.trim()}
118
127
  onClick={() => {
119
- const text = draft.trim();
120
- if (!text) return;
121
- const payload = text.startsWith("@") ? text : `${agentHandle} ${text}`;
122
- void handleInvokeAgent(payload);
123
- setDraft("");
128
+ const handle = session.agentHandle || "@assistant";
129
+ void onSend(`${handle} ${prompt}`);
124
130
  }}
125
131
  >
126
- Ask agent
132
+ {prompt}
127
133
  </button>
128
- </div>
129
- <p className="hint">
130
- Realtime via WebSocket · Agent replies stream in-room · Tool calls appear above
131
- </p>
132
- </form>
133
- </section>
134
+ ))}
135
+ </div>
136
+ {error ? <p className="chat-error">{error}</p> : null}
137
+ </div>
134
138
  );
135
139
  }
136
140
 
137
141
  export function App() {
138
- const client = useMemo(() => {
139
- if (!workerUrl || !memberJwt) return null;
140
- return new FluxyChatClient({
141
- baseUrl: workerUrl,
142
- userId: memberUserId,
143
- token: memberJwt,
144
- });
145
- }, []);
146
-
147
- if (!workerUrl || !memberJwt) {
148
- return (
149
- <main className="shell">
150
- <div className="error-box">
151
- <p>
152
- <strong>Setup required.</strong> Run provisioning against a local FluxyChat worker:
153
- </p>
154
- <pre>
155
- <code>{`# Terminal 1 — from FluxyChat monorepo
156
- pnpm --filter @fluxy-chat/worker dev
157
-
158
- # Terminal 2 — in this project
159
- pnpm setup
160
- pnpm dev`}</code>
161
- </pre>
162
- <p>
163
- Or copy <code>.env.example</code> → <code>.env</code> with credentials from{" "}
164
- <a href="https://fluxychat.com/onboarding" target="_blank" rel="noreferrer">
165
- fluxychat.com/onboarding
166
- </a>
167
- .
168
- </p>
169
- </div>
170
- </main>
171
- );
172
- }
142
+ const session = useMemo(() => loadCliSession() ?? sessionFromEnv(), []);
173
143
 
174
- if (!client) return null;
144
+ if (!session) return <SignInGate />;
175
145
 
176
146
  return (
177
- <main className="shell">
178
- <div className="hero">
179
- <h1>FluxyChat — full stack starter</h1>
180
- <p>
181
- Your app · Worker {workerUrl} ·{" "}
147
+ <main className="app-shell">
148
+ <header className="topbar">
149
+ <div className="brand">
150
+ <img src={`${consoleUrl.replace(/\/$/, "")}/fluxychat-icon.svg`} alt="" width={28} height={28} />
151
+ <span>FluxyChat</span>
152
+ </div>
153
+ <nav className="top-links">
154
+ <span className="meta">{session.projectName || session.projectId || "project"}</span>
182
155
  <a href={`${consoleUrl.replace(/\/$/, "")}/onboarding?from=cli`} target="_blank" rel="noreferrer">
183
- Keep this project
156
+ Open console
184
157
  </a>
185
- </p>
186
- </div>
187
-
188
- <div className="layout">
189
- <FluxyRealtimeProvider client={client}>
190
- <ChatRoom />
158
+ </nav>
159
+ </header>
160
+ <div className="page">
161
+ <FluxyRealtimeProvider
162
+ workerUrl={session.workerUrl}
163
+ authTokenProvider={session.memberJwt}
164
+ userId={session.userId}
165
+ >
166
+ <ChatRoom session={session} />
191
167
  </FluxyRealtimeProvider>
192
-
193
168
  <aside className="side">
194
- <div className="card">
169
+ <section>
195
170
  <h2>Project</h2>
196
171
  <dl>
197
172
  <div>
198
- <dt>Project ID</dt>
199
- <dd>{projectId || "—"}</dd>
173
+ <dt>ID</dt>
174
+ <dd>{session.projectId || "—"}</dd>
200
175
  </div>
201
176
  <div>
202
177
  <dt>Room</dt>
203
- <dd>{roomId}</dd>
178
+ <dd>{session.roomId}</dd>
204
179
  </div>
205
180
  <div>
206
181
  <dt>Agent</dt>
207
- <dd>{agentHandle}{agentId ? ` (${agentId.slice(0, 12)}…)` : ""}</dd>
182
+ <dd>{session.agentHandle}</dd>
208
183
  </div>
209
184
  </dl>
210
- </div>
211
- <div className="card">
212
- <h2>Try next</h2>
213
- <ul style={{ margin: 0, paddingLeft: "1.1rem" }}>
214
- <li>Open a second tab same URL for realtime</li>
215
- <li>Ask the agent about FluxyChat architecture</li>
216
- <li>Manage rooms & agents in the console</li>
185
+ </section>
186
+ <section>
187
+ <h2>Next</h2>
188
+ <ul>
189
+ <li>Send a message, or mention the agent to invoke it</li>
190
+ <li>Open a second tab on this URL for realtime</li>
191
+ <li>
192
+ <a href={`${consoleUrl.replace(/\/$/, "")}/onboarding?from=cli`} target="_blank" rel="noreferrer">
193
+ Continue onboarding in the console
194
+ </a>
195
+ </li>
217
196
  </ul>
218
- </div>
197
+ </section>
219
198
  </aside>
220
199
  </div>
221
200
  </main>
@@ -1,242 +1,278 @@
1
- :root {
2
- font-family: system-ui, -apple-system, sans-serif;
3
- line-height: 1.5;
4
- color: #0f172a;
5
- background: #fdfbf9;
6
- }
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
7
4
 
8
- * {
9
- box-sizing: border-box;
5
+ :root {
6
+ --background: #fdfbf9;
7
+ --foreground: #0e1316;
8
+ --surface: #fdfbf9;
9
+ --surface-muted: #f3f0ec;
10
+ --surface-card: #ffffff;
11
+ --text-primary: #1a1a1a;
12
+ --text-muted: #6b7280;
13
+ --border: #e5e0d8;
14
+ --brand: #ff725e;
15
+ --destructive: #dc2626;
16
+ --ring: #ff725e;
17
+ --radius: 0.5rem;
18
+
19
+ --fluxy-logo-color: #ff6a1a;
20
+ --fluxy-cta-color: #c2410c;
21
+ --fluxy-bubble-sent-bg: #ff6a1a;
22
+ --fluxy-bubble-sent-text: #ffffff;
23
+ --fluxy-bubble-received-bg: #f4f4f5;
24
+ --fluxy-bubble-received-border: #e4e4e7;
25
+ --fluxy-bubble-received-text: #1a1a1a;
26
+ --primary: var(--fluxy-cta-color);
27
+ --primary-foreground: #ffffff;
28
+ --muted: var(--surface-muted);
29
+ --muted-foreground: var(--text-muted);
30
+ }
31
+
32
+ html,
33
+ body,
34
+ #root {
35
+ height: 100%;
10
36
  }
11
37
 
12
38
  body {
13
39
  margin: 0;
40
+ background: var(--background);
41
+ color: var(--foreground);
42
+ font-family: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
43
+ -webkit-font-smoothing: antialiased;
14
44
  }
15
45
 
16
- .shell {
17
- max-width: 880px;
18
- margin: 0 auto;
19
- padding: 1.5rem;
20
- }
21
-
22
- .hero {
23
- margin-bottom: 1rem;
24
- }
25
-
26
- .hero h1 {
27
- font-size: 1.35rem;
28
- margin: 0 0 0.35rem;
46
+ .app-shell {
47
+ min-height: 100%;
48
+ display: flex;
49
+ flex-direction: column;
50
+ background: var(--background);
29
51
  }
30
52
 
31
- .hero p {
32
- margin: 0;
33
- font-size: 0.875rem;
34
- color: #64748b;
53
+ .topbar {
54
+ height: 56px;
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: space-between;
58
+ padding: 0 20px;
59
+ border-bottom: 1px solid var(--border);
60
+ background: var(--surface-card);
35
61
  }
36
62
 
37
- .hero a {
38
- color: #c2410c;
63
+ .brand {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 8px;
67
+ font-weight: 650;
68
+ font-size: 15px;
39
69
  }
40
70
 
41
- .layout {
42
- display: grid;
43
- gap: 1rem;
71
+ .top-links {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 14px;
75
+ font-size: 13px;
44
76
  }
45
77
 
46
- @media (min-width: 900px) {
47
- .layout {
48
- grid-template-columns: 1fr 240px;
49
- align-items: start;
50
- }
78
+ .top-links a,
79
+ .side a {
80
+ color: var(--fluxy-cta-color);
81
+ font-weight: 500;
82
+ text-decoration: none;
51
83
  }
52
84
 
53
- .chat-panel {
54
- display: flex;
55
- flex-direction: column;
56
- min-height: 520px;
57
- border: 1px solid #e7e5e4;
58
- border-radius: 16px;
59
- background: #fff;
60
- overflow: hidden;
61
- box-shadow: 0 8px 30px rgb(15 23 42 / 6%);
85
+ .top-links a:hover,
86
+ .side a:hover {
87
+ text-decoration: underline;
62
88
  }
63
89
 
64
- .chat-header {
65
- display: flex;
66
- justify-content: space-between;
67
- align-items: center;
68
- padding: 0.75rem 1rem;
69
- border-bottom: 1px solid #e7e5e4;
70
- background: #0f172a;
71
- color: #fff;
90
+ .meta {
91
+ color: var(--text-muted);
72
92
  }
73
93
 
74
- .status {
75
- font-size: 0.75rem;
76
- opacity: 0.85;
94
+ .page {
95
+ flex: 1;
96
+ display: grid;
97
+ grid-template-columns: minmax(0, 1fr) 280px;
98
+ min-height: 0;
77
99
  }
78
100
 
79
- .messages {
80
- flex: 1;
81
- overflow-y: auto;
82
- list-style: none;
83
- margin: 0;
84
- padding: 1rem;
101
+ .chat-column {
85
102
  display: flex;
86
103
  flex-direction: column;
87
- gap: 0.5rem;
104
+ min-width: 0;
105
+ padding: 16px 20px 20px;
106
+ gap: 12px;
88
107
  }
89
108
 
90
- .message {
91
- display: flex;
92
- flex-direction: column;
93
- gap: 0.15rem;
94
- padding: 0.6rem 0.85rem;
95
- border-radius: 10px;
96
- background: #f8fafc;
97
- max-width: 85%;
109
+ .status-row {
110
+ font-size: 12px;
111
+ color: var(--text-muted);
98
112
  }
99
113
 
100
- .message.self {
101
- align-self: flex-end;
102
- background: #fff7ed;
103
- border: 1px solid #fed7aa;
114
+ .status-dot {
115
+ width: 8px;
116
+ height: 8px;
117
+ border-radius: 99px;
118
+ display: inline-block;
104
119
  }
120
+ .status-dot.on { background: #16a34a; }
121
+ .status-dot.off { background: #d97706; }
105
122
 
106
- .message.agent {
107
- background: #f1f5f9;
108
- border: 1px solid #e2e8f0;
123
+ .chat-frame {
124
+ flex: 1;
125
+ min-height: 420px;
126
+ background: var(--surface-card);
127
+ border: 1px solid var(--border);
128
+ border-radius: 16px;
129
+ overflow: hidden;
130
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
109
131
  }
110
132
 
111
- .author {
112
- font-size: 0.7rem;
113
- color: #64748b;
114
- font-weight: 600;
133
+ .chat-frame > div {
134
+ height: 100% !important;
135
+ max-width: none !important;
136
+ border: none !important;
137
+ border-radius: 0 !important;
115
138
  }
116
139
 
117
- .composer {
140
+ .prompts {
118
141
  display: flex;
119
- flex-direction: column;
120
- gap: 0.5rem;
121
- padding: 0.75rem;
122
- border-top: 1px solid #e7e5e4;
142
+ flex-wrap: wrap;
143
+ gap: 8px;
123
144
  }
124
145
 
125
- .composer-row {
126
- display: flex;
127
- gap: 0.5rem;
146
+ .prompts button {
147
+ border: 1px solid var(--border);
148
+ background: var(--surface-card);
149
+ color: var(--foreground);
150
+ border-radius: 999px;
151
+ padding: 6px 12px;
152
+ font-size: 12px;
153
+ cursor: pointer;
128
154
  }
129
155
 
130
- .composer input {
131
- flex: 1;
132
- border: 1px solid #cbd5e1;
133
- border-radius: 10px;
134
- padding: 0.55rem 0.75rem;
156
+ .prompts button:hover {
157
+ border-color: var(--fluxy-cta-color);
158
+ color: var(--fluxy-cta-color);
135
159
  }
136
160
 
137
- .composer button {
138
- border: none;
139
- border-radius: 10px;
140
- background: #c2410c;
141
- color: #fff;
142
- padding: 0.55rem 1rem;
143
- cursor: pointer;
144
- font-weight: 600;
161
+ .chat-error {
162
+ color: var(--destructive);
163
+ font-size: 13px;
145
164
  }
146
165
 
147
- .composer button.secondary {
148
- background: #0f172a;
166
+ .side {
167
+ border-left: 1px solid var(--border);
168
+ background: var(--surface-muted);
169
+ padding: 20px 16px;
170
+ display: flex;
171
+ flex-direction: column;
172
+ gap: 16px;
149
173
  }
150
174
 
151
- .composer button:disabled {
152
- opacity: 0.55;
153
- cursor: not-allowed;
175
+ .side section {
176
+ background: var(--surface-card);
177
+ border: 1px solid var(--border);
178
+ border-radius: 12px;
179
+ padding: 14px;
154
180
  }
155
181
 
156
- .hint {
157
- font-size: 0.75rem;
158
- color: #64748b;
182
+ .side h2 {
183
+ font-size: 11px;
184
+ text-transform: uppercase;
185
+ letter-spacing: 0.08em;
186
+ color: var(--text-muted);
187
+ margin: 0 0 10px;
159
188
  }
160
189
 
161
- .tools {
162
- margin: 0 1rem 1rem;
163
- padding: 0.75rem;
164
- border-radius: 10px;
165
- border: 1px solid #e2e8f0;
166
- background: #f8fafc;
190
+ .side dl {
191
+ display: flex;
192
+ flex-direction: column;
193
+ gap: 8px;
194
+ margin: 0;
167
195
  }
168
196
 
169
- .tools h3 {
170
- margin: 0 0 0.5rem;
171
- font-size: 0.7rem;
172
- letter-spacing: 0.06em;
173
- text-transform: uppercase;
174
- color: #64748b;
197
+ .side dt {
198
+ font-size: 11px;
199
+ color: var(--text-muted);
175
200
  }
176
201
 
177
- .tools ul {
202
+ .side dd {
178
203
  margin: 0;
179
- padding-left: 1rem;
180
- font-size: 0.8rem;
204
+ font-size: 13px;
205
+ word-break: break-all;
181
206
  }
182
207
 
183
- .side {
208
+ .side ul {
209
+ margin: 0;
210
+ padding-left: 1.1rem;
211
+ color: var(--text-muted);
212
+ font-size: 13px;
184
213
  display: flex;
185
214
  flex-direction: column;
186
- gap: 0.75rem;
215
+ gap: 8px;
187
216
  }
188
217
 
189
- .card {
190
- border: 1px solid #e7e5e4;
191
- border-radius: 12px;
192
- padding: 0.85rem 1rem;
193
- background: #fff;
194
- font-size: 0.8rem;
218
+ .signin-card {
219
+ margin: auto;
220
+ max-width: 440px;
221
+ width: calc(100% - 32px);
222
+ background: var(--surface-card);
223
+ border: 1px solid var(--border);
224
+ border-radius: 16px;
225
+ padding: 32px;
226
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
195
227
  }
196
228
 
197
- .card h2 {
198
- margin: 0 0 0.5rem;
199
- font-size: 0.75rem;
229
+ .eyebrow {
230
+ font-size: 11px;
231
+ font-weight: 650;
232
+ letter-spacing: 0.08em;
200
233
  text-transform: uppercase;
201
- letter-spacing: 0.05em;
202
- color: #64748b;
203
- }
204
-
205
- .card dl {
206
- margin: 0;
207
- display: grid;
208
- gap: 0.35rem;
234
+ color: var(--fluxy-cta-color);
235
+ margin: 0 0 8px;
209
236
  }
210
237
 
211
- .card dt {
212
- color: #94a3b8;
213
- font-size: 0.7rem;
238
+ .signin-card h1 {
239
+ margin: 0 0 8px;
240
+ font-size: 24px;
214
241
  }
215
242
 
216
- .card dd {
217
- margin: 0;
218
- word-break: break-all;
243
+ .signin-card p {
244
+ color: var(--text-muted);
245
+ font-size: 14px;
246
+ line-height: 1.5;
219
247
  }
220
248
 
221
- .card a {
222
- color: #c2410c;
249
+ .btn-primary {
250
+ display: inline-flex;
251
+ margin-top: 16px;
252
+ background: var(--fluxy-cta-color);
253
+ color: #fff;
254
+ border-radius: 10px;
255
+ padding: 10px 16px;
256
+ font-weight: 600;
257
+ font-size: 14px;
258
+ text-decoration: none;
223
259
  }
224
260
 
225
- .error-box {
226
- border: 1px solid #fecaca;
227
- background: #fef2f2;
228
- color: #991b1b;
229
- border-radius: 12px;
230
- padding: 1rem;
231
- font-size: 0.875rem;
261
+ .btn-primary:hover {
262
+ background: #9a3412;
232
263
  }
233
264
 
234
- .error-box code {
235
- font-size: 0.8rem;
265
+ .fine-print {
266
+ margin-top: 12px;
267
+ font-size: 12px !important;
236
268
  }
237
269
 
238
- .typing {
239
- padding: 0 1rem 0.5rem;
240
- font-size: 0.75rem;
241
- color: #64748b;
270
+ @media (max-width: 860px) {
271
+ .page {
272
+ grid-template-columns: 1fr;
273
+ }
274
+ .side {
275
+ border-left: none;
276
+ border-top: 1px solid var(--border);
277
+ }
242
278
  }
@@ -0,0 +1,54 @@
1
+ const STORAGE_KEY = "fluxy-cli-session";
2
+
3
+ export interface CliSession {
4
+ workerUrl: string;
5
+ memberJwt: string;
6
+ roomId: string;
7
+ agentId: string;
8
+ agentHandle: string;
9
+ projectId: string;
10
+ userId: string;
11
+ projectName?: string;
12
+ consoleUrl?: string;
13
+ }
14
+
15
+ function isCliSession(value: unknown): value is CliSession {
16
+ if (!value || typeof value !== "object") return false;
17
+ const v = value as Record<string, unknown>;
18
+ return typeof v.memberJwt === "string" && v.memberJwt.length > 0 && typeof v.workerUrl === "string";
19
+ }
20
+
21
+ export function readCliSessionFromHash(): CliSession | null {
22
+ if (typeof window === "undefined") return null;
23
+ const hash = window.location.hash.replace(/^#/, "");
24
+ const raw = new URLSearchParams(hash).get("fluxy_cli") || (hash.startsWith("fluxy_cli=") ? hash.slice("fluxy_cli=".length) : "");
25
+ if (!raw) return null;
26
+ try {
27
+ const parsed = JSON.parse(atob(decodeURIComponent(raw))) as unknown;
28
+ if (!isCliSession(parsed)) return null;
29
+ window.history.replaceState(null, "", window.location.pathname + window.location.search);
30
+ return parsed;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ export function loadCliSession(): CliSession | null {
37
+ const fromHash = readCliSessionFromHash();
38
+ if (fromHash) {
39
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(fromHash));
40
+ return fromHash;
41
+ }
42
+ try {
43
+ const stored = sessionStorage.getItem(STORAGE_KEY);
44
+ if (!stored) return null;
45
+ const parsed = JSON.parse(stored) as unknown;
46
+ return isCliSession(parsed) ? parsed : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ export function clearCliSession() {
53
+ sessionStorage.removeItem(STORAGE_KEY);
54
+ }
@@ -0,0 +1,41 @@
1
+ export default {
2
+ content: [
3
+ "./index.html",
4
+ "./src/**/*.{ts,tsx}",
5
+ "./node_modules/@fluxy-chat/ui/dist/**/*.{js,mjs}",
6
+ ],
7
+ theme: {
8
+ extend: {
9
+ colors: {
10
+ background: "var(--background)",
11
+ foreground: "var(--foreground)",
12
+ border: "var(--border)",
13
+ muted: {
14
+ DEFAULT: "var(--surface-muted)",
15
+ foreground: "var(--text-muted)",
16
+ },
17
+ primary: {
18
+ DEFAULT: "var(--fluxy-cta-color)",
19
+ foreground: "#ffffff",
20
+ },
21
+ secondary: {
22
+ DEFAULT: "var(--surface-muted)",
23
+ foreground: "var(--foreground)",
24
+ },
25
+ card: {
26
+ DEFAULT: "var(--surface-card)",
27
+ foreground: "var(--foreground)",
28
+ },
29
+ destructive: {
30
+ DEFAULT: "var(--destructive)",
31
+ foreground: "#ffffff",
32
+ },
33
+ ring: "var(--ring)",
34
+ },
35
+ borderRadius: {
36
+ lg: "var(--radius)",
37
+ },
38
+ },
39
+ },
40
+ plugins: [],
41
+ };