mop-agent 0.1.6 → 0.1.8

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/README.md CHANGED
@@ -5,16 +5,16 @@ through MOP-FLOW. It stores project memory, performs semantic recall and
5
5
  consolidation, serves grounded chat, and can request approved actions from a
6
6
  linked FLOW node.
7
7
 
8
- > **Release status:** npm package `mop-agent@0.1.6` contains the corrected VPS
9
- > installer and first-run Admin/Assistant flow. The canonical installation command is
10
- > exactly `npx mop-agent`.
8
+ > **Release status:** npm package `mop-agent@0.1.8` contains the corrected VPS
9
+ > installer, first-run Admin/Assistant flow, and shared retro application shell.
10
+ > The canonical installation command is exactly `npx mop-agent`.
11
11
 
12
12
  ## Current status
13
13
 
14
14
  The application core through Fasa 7 foundation is implemented: reverse-WSS
15
15
  project links, SQLite + sqlite-vec storage, Better Auth, semantic recall,
16
- provider settings, consolidation, approval-based write-back, Telegram and
17
- Discord adapters, skills, graph UI, execution backends, and team invites.
16
+ admin-only provider/user settings, consolidation, approval-based write-back,
17
+ Telegram and Discord adapters, skills, graph UI, execution backends, and user invites.
18
18
 
19
19
  The npm bootstrap stages the packaged application durably at `/opt/mop-agent`,
20
20
  uses the proven SQLite + sqlite-vec backend, and asks for sudo only for specific
@@ -3,5 +3,13 @@ import { auth, ownerExists } from "@/lib/auth";
3
3
 
4
4
  export async function GET(req: Request): Promise<Response> {
5
5
  const session = await auth.api.getSession({ headers: req.headers });
6
- return Response.json({ ownerExists: ownerExists(), authenticated: !!session });
6
+ return Response.json(
7
+ { ownerExists: ownerExists(), authenticated: !!session },
8
+ {
9
+ headers: {
10
+ "Cache-Control": "private, no-store, no-cache, max-age=0, must-revalidate",
11
+ "Vary": "Cookie",
12
+ },
13
+ },
14
+ );
7
15
  }
@@ -1,7 +1,20 @@
1
1
  import type { ReactNode } from "react";
2
+ import { getRole } from "@/lib/auth";
2
3
  import { requirePageSession } from "@/lib/page-auth";
4
+ import { AppShell } from "@/components/AppShell";
5
+
6
+ export const dynamic = "force-dynamic";
7
+ export const revalidate = 0;
3
8
 
4
9
  export default async function AssistantLayout({ children }: { children: ReactNode }) {
5
- await requirePageSession();
6
- return children;
10
+ const session = await requirePageSession();
11
+ return (
12
+ <AppShell viewer={{
13
+ name: session.user.name || session.user.email,
14
+ email: session.user.email,
15
+ role: getRole(session.user.id) ?? "member",
16
+ }}>
17
+ {children}
18
+ </AppShell>
19
+ );
7
20
  }
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import type { CSSProperties } from "react";
3
4
  import { useEffect, useRef, useState } from "react";
4
- import { signOut } from "@/lib/auth-client";
5
5
 
6
6
  type Turn = { role: "user" | "assistant"; content: string };
7
7
  type Project = { id: string; name: string; status: string };
@@ -75,122 +75,88 @@ export default function AssistantPage() {
75
75
  setBusy(false);
76
76
  }
77
77
 
78
- async function logout() {
79
- await signOut();
80
- window.location.replace("/setup");
81
- }
82
-
83
78
  return (
84
- <main className="mop-assistant-shell" style={appShell}>
85
- <aside className="mop-assistant-sidebar" style={sidebar}>
86
- <a href="/assistant" style={brand}><span style={brandMark}>M</span><strong>MOP-AGENT</strong></a>
87
- <nav style={{ display: "grid", gap: 6, marginTop: 34 }}>
88
- <a href="/assistant" style={{ ...navItem, ...navActive }}>✦ Assistant</a>
89
- <a href="/brain" style={navItem}>◉ Brain</a>
90
- <a href="/settings" style={navItem}>⚙ Providers</a>
91
- <a href="/team" style={navItem}>♙ Team</a>
92
- </nav>
93
-
94
- <div style={{ marginTop: "auto", display: "grid", gap: 10 }}>
95
- {!provider.configured && (
96
- <a href="/settings" style={setupCard}>
97
- <strong style={{ color: "#fef9e1" }}>Connect an AI model</strong>
98
- <span style={{ fontSize: 12, lineHeight: 1.45 }}>Offline demo is active. Add OpenRouter or Anthropic for full answers.</span>
99
- </a>
100
- )}
101
- <button onClick={logout} style={accountButton} title="Sign out">
102
- <span style={avatar}>{name.slice(0, 1).toUpperCase()}</span>
103
- <span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>{name}</span>
104
- <span style={{ marginLeft: "auto", opacity: .55 }}>↪</span>
105
- </button>
79
+ <section className="mop-assistant-page">
80
+ <div className="mop-assistant-toolbar">
81
+ <div>
82
+ <strong style={{ fontFamily: '"SFMono-Regular", Consolas, monospace', color: "#742220" }}>LIVE ASSISTANT</strong>
83
+ <span style={{ color: "rgba(45,74,62,.62)", marginLeft: 10, fontSize: 12 }}>
84
+ {provider.configured ? `${provider.provider}${provider.model ? ` · ${provider.model}` : ""}` : "offline demo"}
85
+ </span>
106
86
  </div>
107
- </aside>
87
+ <label style={{ color: "#2d4a3e", fontSize: 12 }}>
88
+ MEMORY SCOPE&nbsp;
89
+ <select value={selectedProject} onChange={(e) => setSelectedProject(e.target.value)} style={selectStyle}>
90
+ <option value="">All memory</option>
91
+ {projects.map((project) => <option key={project.id} value={project.id}>{project.name}</option>)}
92
+ </select>
93
+ </label>
94
+ </div>
108
95
 
109
- <section style={workspace}>
110
- <header style={topbar}>
111
- <div>
112
- <strong>Assistant</strong>
113
- <span style={{ color: "rgba(45,74,62,.68)", marginLeft: 9, fontSize: 12 }}>{provider.configured ? `${provider.provider}${provider.model ? ` · ${provider.model}` : ""}` : "offline demo"}</span>
114
- </div>
115
- <label style={{ color: "#2d4a3e", fontSize: 12 }}>
116
- Memory scope&nbsp;
117
- <select value={selectedProject} onChange={(e) => setSelectedProject(e.target.value)} style={selectStyle}>
118
- <option value="">All memory</option>
119
- {projects.map((project) => <option key={project.id} value={project.id}>{project.name}</option>)}
120
- </select>
121
- </label>
122
- </header>
123
-
124
- <div style={conversation}>
125
- {turns.length === 0 ? (
126
- <div style={welcome}>
127
- <div style={assistantOrb}>✦</div>
128
- <p style={{ color: "#742220", fontSize: 12, fontWeight: 800, letterSpacing: ".13em" }}>MOP-AGENT IS READY</p>
129
- <h1 style={{ fontSize: "clamp(28px, 4vw, 42px)", margin: "8px 0 12px" }}>What are we working on, {name.split(" ")[0]}?</h1>
130
- <p style={{ color: "rgba(45,74,62,.72)", maxWidth: 610, lineHeight: 1.65 }}>
131
- Start talking immediately. Link projects when you want MOP-AGENT to remember their state and work across them.
132
- </p>
133
- <div className="mop-prompt-grid" style={promptGrid}>
134
- {["Help me plan today’s work", "What can MOP-AGENT do?", "Summarize what you remember", "Plan a new software project"].map((prompt) => (
135
- <button key={prompt} onClick={() => send(prompt)} style={promptCard}>{prompt}<span>→</span></button>
136
- ))}
137
- </div>
138
- {projects.length === 0 && (
139
- <p style={{ fontSize: 13, color: "rgba(45,74,62,.68)", marginTop: 24 }}>
140
- No project linked yet—this does not block chat. <a href="/brain" style={{ color: "#742220" }}>Link one from Brain →</a>
141
- </p>
142
- )}
143
- </div>
144
- ) : (
145
- <div style={{ width: "min(100%, 820px)", margin: "0 auto", padding: "28px 0 160px" }}>
146
- {turns.map((turn, index) => (
147
- <article key={index} style={{ display: "grid", gridTemplateColumns: "34px 1fr", gap: 13, marginBottom: 26 }}>
148
- <span style={turn.role === "assistant" ? botAvatar : userAvatar}>{turn.role === "assistant" ? "✦" : name.slice(0, 1).toUpperCase()}</span>
149
- <div>
150
- <strong style={{ fontSize: 13, color: turn.role === "assistant" ? "#742220" : "#2d4a3e" }}>{turn.role === "assistant" ? "MOP-AGENT" : "You"}</strong>
151
- <div style={{ whiteSpace: "pre-wrap", lineHeight: 1.7, marginTop: 6, color: "#2d4a3e" }}>{turn.content || "Thinking…"}</div>
152
- </div>
153
- </article>
96
+ <div className="mop-assistant-conversation">
97
+ {turns.length === 0 ? (
98
+ <div className="mop-assistant-welcome">
99
+ <div style={assistantLogo}><img src="/icon.svg" alt="MOP-AGENT" /></div>
100
+ <p style={{ color: "#742220", fontSize: 11, fontWeight: 900, letterSpacing: ".16em" }}>MOP-AGENT IS READY</p>
101
+ <h1 style={{ fontFamily: '"SFMono-Regular", Consolas, monospace', fontSize: "clamp(26px, 4vw, 40px)", margin: "8px 0 12px" }}>
102
+ What are we working on, {name.split(" ")[0]}?
103
+ </h1>
104
+ <p style={{ color: "rgba(45,74,62,.7)", maxWidth: 610, lineHeight: 1.65 }}>
105
+ Start talking immediately. Link projects when you want MOP-AGENT to remember their state and work across them.
106
+ </p>
107
+ <div className="mop-prompt-grid" style={promptGrid}>
108
+ {["Help me plan today’s work", "What can MOP-AGENT do?", "Summarize what you remember", "Plan a new software project"].map((prompt) => (
109
+ <button key={prompt} onClick={() => send(prompt)} style={promptCard}>{prompt}<span>→</span></button>
154
110
  ))}
155
- <div ref={endRef} />
156
111
  </div>
157
- )}
158
- </div>
159
-
160
- <div style={composerWrap}>
161
- <div style={composer}>
162
- <textarea value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }} placeholder="Message MOP-AGENT…" rows={1} style={textarea} />
163
- <button onClick={() => send()} disabled={busy || !input.trim()} style={{ ...sendButton, opacity: busy || !input.trim() ? .45 : 1 }}>↑</button>
112
+ {projects.length === 0 && (
113
+ <p style={{ fontSize: 13, color: "rgba(45,74,62,.68)", marginTop: 24 }}>
114
+ No project linked yet—this does not block chat. <a href="/brain" style={{ color: "#742220" }}>Link one from Brain →</a>
115
+ </p>
116
+ )}
164
117
  </div>
165
- <div style={{ textAlign: "center", color: "rgba(45,74,62,.62)", fontSize: 11, marginTop: 8 }}>
166
- {providerUsed ? `Answered by ${providerUsed} · ` : ""}{selectedProject ? "Selected project memory" : "Cross-project memory"}
118
+ ) : (
119
+ <div style={{ width: "min(100%, 820px)", margin: "0 auto", padding: "28px 0 160px" }}>
120
+ {turns.map((turn, index) => (
121
+ <article key={index} style={{ display: "grid", gridTemplateColumns: "34px 1fr", gap: 13, marginBottom: 26 }}>
122
+ <span style={turn.role === "assistant" ? botAvatar : userAvatar}>{turn.role === "assistant" ? "✦" : name.slice(0, 1).toUpperCase()}</span>
123
+ <div>
124
+ <strong style={{ fontSize: 13, color: turn.role === "assistant" ? "#742220" : "#2d4a3e" }}>{turn.role === "assistant" ? "MOP-AGENT" : "You"}</strong>
125
+ <div style={{ whiteSpace: "pre-wrap", lineHeight: 1.7, marginTop: 6, color: "#2d4a3e" }}>{turn.content || "Thinking…"}</div>
126
+ </div>
127
+ </article>
128
+ ))}
129
+ <div ref={endRef} />
167
130
  </div>
131
+ )}
132
+ </div>
133
+
134
+ <div className="mop-assistant-composer-wrap">
135
+ <div style={composer}>
136
+ <textarea
137
+ value={input}
138
+ onChange={(e) => setInput(e.target.value)}
139
+ onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }}
140
+ placeholder="Message MOP-AGENT…"
141
+ rows={1}
142
+ style={textarea}
143
+ />
144
+ <button onClick={() => send()} disabled={busy || !input.trim()} style={{ ...sendButton, opacity: busy || !input.trim() ? .45 : 1 }}>↑</button>
145
+ </div>
146
+ <div style={{ textAlign: "center", color: "rgba(45,74,62,.62)", fontSize: 11, marginTop: 8 }}>
147
+ {providerUsed ? `Answered by ${providerUsed} · ` : ""}{selectedProject ? "Selected project memory" : "Cross-project memory"}
168
148
  </div>
169
- </section>
170
- </main>
149
+ </div>
150
+ </section>
171
151
  );
172
152
  }
173
153
 
174
- const appShell: React.CSSProperties = { minHeight: "100vh", display: "grid", gridTemplateColumns: "235px 1fr", background: "#fef9e1" };
175
- const sidebar: React.CSSProperties = { padding: "22px 15px", borderRight: "1px solid #20382f", background: "#2d4a3e", display: "flex", flexDirection: "column", minHeight: "calc(100vh - 44px)" };
176
- const brand: React.CSSProperties = { display: "flex", alignItems: "center", gap: 10, color: "#fef9e1", textDecoration: "none", padding: "4px 8px" };
177
- const brandMark: React.CSSProperties = { width: 28, height: 28, borderRadius: 8, display: "grid", placeItems: "center", background: "#742220", color: "#fef9e1", fontSize: 13 };
178
- const navItem: React.CSSProperties = { color: "rgba(254,249,225,.76)", textDecoration: "none", padding: "10px 12px", borderRadius: 8, fontSize: 14 };
179
- const navActive: React.CSSProperties = { color: "#fef9e1", background: "#742220", boxShadow: "inset 2px 0 #fef9e1" };
180
- const setupCard: React.CSSProperties = { display: "grid", gap: 5, padding: 12, border: "1px solid rgba(254,249,225,.45)", borderRadius: 10, background: "#742220", color: "rgba(254,249,225,.74)", textDecoration: "none" };
181
- const accountButton: React.CSSProperties = { display: "flex", alignItems: "center", gap: 9, padding: 8, border: 0, borderRadius: 9, background: "transparent", color: "#fef9e1", cursor: "pointer", textAlign: "left" };
182
- const avatar: React.CSSProperties = { width: 28, height: 28, borderRadius: 8, display: "grid", placeItems: "center", background: "#742220", color: "#fef9e1", fontSize: 12 };
183
- const workspace: React.CSSProperties = { minWidth: 0, minHeight: "100vh", position: "relative", display: "flex", flexDirection: "column", background: "radial-gradient(circle at 50% 4%, #fffdf2 0, #fef9e1 58%)" };
184
- const topbar: React.CSSProperties = { height: 62, padding: "0 24px", display: "flex", alignItems: "center", justifyContent: "space-between", borderBottom: "1px solid rgba(45,74,62,.25)", background: "rgba(254,249,225,.88)", backdropFilter: "blur(16px)" };
185
- const selectStyle: React.CSSProperties = { color: "#2d4a3e", border: "1px solid rgba(45,74,62,.38)", borderRadius: 7, padding: "6px 8px", background: "#fffdf2" };
186
- const conversation: React.CSSProperties = { flex: 1, overflowY: "auto", padding: "0 28px" };
187
- const welcome: React.CSSProperties = { minHeight: "calc(100vh - 220px)", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", textAlign: "center", paddingBottom: 60 };
188
- const assistantOrb: React.CSSProperties = { width: 58, height: 58, borderRadius: 18, display: "grid", placeItems: "center", fontSize: 24, color: "#fef9e1", background: "#742220", boxShadow: "0 15px 55px rgba(116,34,32,.22)" };
189
- const promptGrid: React.CSSProperties = { width: "min(100%, 650px)", display: "grid", gridTemplateColumns: "repeat(2,minmax(0,1fr))", gap: 10, marginTop: 28 };
190
- const promptCard: React.CSSProperties = { display: "flex", justifyContent: "space-between", padding: "14px 15px", borderRadius: 10, border: "1px solid rgba(45,74,62,.3)", background: "#fffdf2", color: "#2d4a3e", cursor: "pointer", textAlign: "left" };
191
- const botAvatar: React.CSSProperties = { width: 32, height: 32, borderRadius: 9, display: "grid", placeItems: "center", background: "#742220", color: "#fef9e1" };
192
- const userAvatar: React.CSSProperties = { ...botAvatar, background: "#2d4a3e", color: "#fef9e1", fontSize: 12 };
193
- const composerWrap: React.CSSProperties = { position: "absolute", left: 0, right: 0, bottom: 0, padding: "28px 30px 18px", background: "linear-gradient(transparent,#fef9e1 28%)" };
194
- const composer: React.CSSProperties = { width: "min(calc(100% - 32px), 800px)", margin: "0 auto", display: "flex", alignItems: "flex-end", gap: 10, padding: "10px 10px 10px 15px", border: "1px solid rgba(45,74,62,.42)", borderRadius: 14, background: "#fffdf2", boxShadow: "0 15px 45px rgba(45,74,62,.16)" };
195
- const textarea: React.CSSProperties = { flex: 1, resize: "none", border: 0, outline: 0, background: "transparent", color: "#2d4a3e", font: "inherit", lineHeight: 1.55, padding: "5px 0" };
196
- const sendButton: React.CSSProperties = { width: 34, height: 34, border: 0, borderRadius: 9, background: "#742220", color: "#fef9e1", fontSize: 18, cursor: "pointer" };
154
+ const selectStyle: CSSProperties = { color: "#2d4a3e", border: "1px solid rgba(45,74,62,.42)", padding: "6px 8px", background: "#fffdf2" };
155
+ const assistantLogo: CSSProperties = { width: 86, height: 86, display: "grid", placeItems: "center", overflow: "hidden", background: "#2d4a3e", border: "2px solid #742220", boxShadow: "5px 5px 0 rgba(45,74,62,.18)" };
156
+ const promptGrid: CSSProperties = { width: "min(100%, 650px)", display: "grid", gridTemplateColumns: "repeat(2,minmax(0,1fr))", gap: 10, marginTop: 28 };
157
+ const promptCard: CSSProperties = { display: "flex", justifyContent: "space-between", padding: "14px 15px", border: "1px solid rgba(45,74,62,.38)", borderBottomWidth: 3, background: "#fffdf2", color: "#2d4a3e", cursor: "pointer", textAlign: "left" };
158
+ const botAvatar: CSSProperties = { width: 32, height: 32, display: "grid", placeItems: "center", background: "#742220", color: "#fef9e1" };
159
+ const userAvatar: CSSProperties = { ...botAvatar, background: "#2d4a3e", fontSize: 12 };
160
+ const composer: CSSProperties = { width: "min(calc(100% - 32px), 800px)", margin: "0 auto", display: "flex", alignItems: "flex-end", gap: 10, padding: "10px 10px 10px 15px", border: "1px solid rgba(45,74,62,.48)", borderBottomWidth: 4, background: "#fffdf2", boxShadow: "4px 4px 0 rgba(45,74,62,.13)" };
161
+ const textarea: CSSProperties = { flex: 1, resize: "none", border: 0, outline: 0, boxShadow: "none", background: "transparent", color: "#2d4a3e", font: "inherit", lineHeight: 1.55, padding: "5px 0" };
162
+ const sendButton: CSSProperties = { width: 34, height: 34, border: 0, background: "#742220", color: "#fef9e1", fontSize: 18, cursor: "pointer" };
@@ -16,7 +16,7 @@ export default function ProjectBrainPage({ params }: { params: Promise<{ project
16
16
  }, [projectId]);
17
17
 
18
18
  return (
19
- <main style={{ maxWidth: 900, margin: "0 auto", padding: "40px 24px" }}>
19
+ <main className="mop-page">
20
20
  <a href="/brain" style={{ color: "#742220" }}>← Brain</a>
21
21
  <h1 style={{ fontSize: 24 }}>{projectId}</h1>
22
22
  <a href={`/chat/${projectId}`} style={{ color: "#742220" }}>💬 Chat with this project →</a>
@@ -37,7 +37,7 @@ export default function GraphPage() {
37
37
  );
38
38
 
39
39
  return (
40
- <main style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
40
+ <main style={{ height: "calc(100vh - 70px)", display: "flex", flexDirection: "column" }}>
41
41
  <div style={{ padding: "12px 16px" }}>
42
42
  <a href="/brain" style={{ color: "#742220" }}>← Brain</a>{" "}
43
43
  <strong>Knowledge Graph</strong>{" "}
@@ -1,7 +1,20 @@
1
1
  import type { ReactNode } from "react";
2
+ import { getRole } from "@/lib/auth";
2
3
  import { requirePageSession } from "@/lib/page-auth";
4
+ import { AppShell } from "@/components/AppShell";
5
+
6
+ export const dynamic = "force-dynamic";
7
+ export const revalidate = 0;
3
8
 
4
9
  export default async function BrainLayout({ children }: { children: ReactNode }) {
5
- await requirePageSession();
6
- return children;
10
+ const session = await requirePageSession();
11
+ return (
12
+ <AppShell viewer={{
13
+ name: session.user.name || session.user.email,
14
+ email: session.user.email,
15
+ role: getRole(session.user.id) ?? "member",
16
+ }}>
17
+ {children}
18
+ </AppShell>
19
+ );
7
20
  }
@@ -67,14 +67,15 @@ export default function BrainPage() {
67
67
  }
68
68
 
69
69
  return (
70
- <main style={{ maxWidth: 900, margin: "0 auto", padding: "40px 24px" }}>
71
- <p style={{ margin: 0 }}><a href="/assistant" style={{ color: "#742220" }}>← Assistant</a></p>
72
- <h1 style={{ fontSize: 24 }}>🧠 Brain</h1>
73
- <p style={{ opacity: 0.65 }}>
74
- Main Brain + linked project brains. <a href="/brain/graph" style={{ color: "#742220" }}>🕸 Knowledge graph →</a>{" "}
75
- <a href="/settings" style={{ color: "#742220" }}>⚙️ Settings →</a>{" "}
76
- <a href="/team" style={{ color: "#742220" }}>👥 Team →</a>
77
- </p>
70
+ <main className="mop-page">
71
+ <header className="mop-page-heading">
72
+ <div>
73
+ <p className="mop-page-kicker">PERSISTENT MEMORY</p>
74
+ <h1>Brain</h1>
75
+ <p>Main Brain and every linked project memory in one place.</p>
76
+ </div>
77
+ <a href="/brain/graph" style={{ ...btn, textDecoration: "none" }}>KNOWLEDGE GRAPH →</a>
78
+ </header>
78
79
 
79
80
  <div style={{ margin: "20px 0", display: "flex", gap: 12, alignItems: "center" }}>
80
81
  <button onClick={genCode} style={btn}>+ Link project</button>
@@ -85,7 +86,7 @@ export default function BrainPage() {
85
86
  )}
86
87
  </div>
87
88
 
88
- <section style={{ margin: "8px 0 24px", border: "1px solid rgba(45,74,62,.28)", borderRadius: 8, padding: 16, background: "#fffdf2" }}>
89
+ <section className="mop-panel" style={{ margin: "8px 0 24px", padding: 16 }}>
89
90
  <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
90
91
  <h2 style={{ fontSize: 16, margin: 0, opacity: 0.85 }}>🌐 Main Brain ({notes.length})</h2>
91
92
  <button onClick={runConsolidate} disabled={consolidating} style={{ ...btn, background: "#2d4a3e", borderColor: "#2d4a3e" }}>
@@ -71,7 +71,7 @@ export default function ChatPage({ params }: { params: Promise<{ projectId: stri
71
71
  }
72
72
 
73
73
  return (
74
- <main style={{ maxWidth: 760, margin: "0 auto", padding: "32px 24px", display: "flex", flexDirection: "column", height: "90vh" }}>
74
+ <main style={{ maxWidth: 860, margin: "0 auto", padding: "28px 24px", display: "flex", flexDirection: "column", height: "calc(100vh - 70px)" }}>
75
75
  <a href={`/brain/${projectId}`} style={{ color: "#742220" }}>← {projectId}</a>
76
76
  <h1 style={{ fontSize: 20 }}>💬 Chat · {projectId}</h1>
77
77
 
@@ -1,7 +1,20 @@
1
1
  import type { ReactNode } from "react";
2
+ import { getRole } from "@/lib/auth";
2
3
  import { requirePageSession } from "@/lib/page-auth";
4
+ import { AppShell } from "@/components/AppShell";
5
+
6
+ export const dynamic = "force-dynamic";
7
+ export const revalidate = 0;
3
8
 
4
9
  export default async function ChatLayout({ children }: { children: ReactNode }) {
5
- await requirePageSession();
6
- return children;
10
+ const session = await requirePageSession();
11
+ return (
12
+ <AppShell viewer={{
13
+ name: session.user.name || session.user.email,
14
+ email: session.user.email,
15
+ role: getRole(session.user.id) ?? "member",
16
+ }}>
17
+ {children}
18
+ </AppShell>
19
+ );
7
20
  }