mop-agent 0.1.13 → 0.1.14

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,7 +5,7 @@ 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:** release candidate `mop-agent@0.1.13` contains the corrected VPS
8
+ > **Release status:** release candidate `mop-agent@0.1.14` contains the corrected VPS
9
9
  > installer, one-time Admin setup/login flow, and simplified shared application shell
10
10
  > with centered page titles and ChatGPT-inspired navigation.
11
11
  > The canonical installation command is exactly `npx mop-agent`.
@@ -5,6 +5,9 @@ import { useEffect, useRef, useState } from "react";
5
5
  import { useMemoryCore } from "@/components/AppShell";
6
6
 
7
7
  type Turn = { role: "user" | "assistant"; content: string };
8
+ type SavedChat = { id: string; title: string; turns: Turn[]; updatedAt: number };
9
+
10
+ const CHAT_HISTORY_KEY = "mop-agent-chat-history-v1";
8
11
 
9
12
  export default function AssistantPage() {
10
13
  const { projects } = useMemoryCore();
@@ -13,6 +16,9 @@ export default function AssistantPage() {
13
16
  const [input, setInput] = useState("");
14
17
  const [busy, setBusy] = useState(false);
15
18
  const [providerUsed, setProviderUsed] = useState("");
19
+ const [history, setHistory] = useState<SavedChat[]>([]);
20
+ const [historyReady, setHistoryReady] = useState(false);
21
+ const [activeChatId, setActiveChatId] = useState("");
16
22
  const endRef = useRef<HTMLDivElement>(null);
17
23
 
18
24
  useEffect(() => {
@@ -21,10 +27,61 @@ export default function AssistantPage() {
21
27
  }).catch(() => {});
22
28
  }, []);
23
29
 
30
+ useEffect(() => {
31
+ try {
32
+ const stored = JSON.parse(window.localStorage.getItem(CHAT_HISTORY_KEY) ?? "[]");
33
+ if (Array.isArray(stored)) setHistory(stored.slice(0, 30));
34
+ } catch {
35
+ window.localStorage.removeItem(CHAT_HISTORY_KEY);
36
+ } finally {
37
+ setHistoryReady(true);
38
+ }
39
+ }, []);
40
+
41
+ useEffect(() => {
42
+ if (!historyReady) return;
43
+ window.localStorage.setItem(CHAT_HISTORY_KEY, JSON.stringify(history.slice(0, 30)));
44
+ }, [history, historyReady]);
45
+
46
+ useEffect(() => {
47
+ if (!activeChatId || turns.length === 0) return;
48
+ const firstMessage = turns.find((turn) => turn.role === "user")?.content ?? "New conversation";
49
+ setHistory((current) => {
50
+ const updated: SavedChat = {
51
+ id: activeChatId,
52
+ title: firstMessage.slice(0, 54),
53
+ turns,
54
+ updatedAt: Date.now(),
55
+ };
56
+ return [updated, ...current.filter((chat) => chat.id !== activeChatId)].slice(0, 30);
57
+ });
58
+ }, [activeChatId, turns]);
59
+
60
+ function startNewChat() {
61
+ if (busy) return;
62
+ setActiveChatId("");
63
+ setTurns([]);
64
+ setProviderUsed("");
65
+ setInput("");
66
+ }
67
+
68
+ function openChat(chat: SavedChat) {
69
+ if (busy) return;
70
+ setActiveChatId(chat.id);
71
+ setTurns(chat.turns);
72
+ setProviderUsed("");
73
+ }
74
+
75
+ function deleteChat(id: string) {
76
+ if (busy) return;
77
+ setHistory((current) => current.filter((chat) => chat.id !== id));
78
+ if (activeChatId === id) startNewChat();
79
+ }
24
80
 
25
81
  async function send(prefill?: string) {
26
82
  const message = (prefill ?? input).trim();
27
83
  if (!message || busy) return;
84
+ if (!activeChatId) setActiveChatId(`chat-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
28
85
  setInput("");
29
86
  setBusy(true);
30
87
  setTurns((current) => [...current, { role: "user", content: message }, { role: "assistant", content: "" }]);
@@ -68,64 +125,95 @@ export default function AssistantPage() {
68
125
 
69
126
  return (
70
127
  <section className="mop-assistant-page">
71
- <div className="mop-assistant-conversation">
72
- {turns.length === 0 ? (
73
- <div className="mop-assistant-welcome">
74
- <div style={assistantLogo}><img src="/icon.svg" alt="MOP-AGENT" /></div>
75
- <p style={{ color: "#742220", fontSize: 11, fontWeight: 900, letterSpacing: ".16em" }}>MOP-AGENT IS READY</p>
76
- <h1 style={{ fontFamily: '"SFMono-Regular", Consolas, monospace', fontSize: "clamp(26px, 4vw, 40px)", margin: "8px 0 12px" }}>
77
- What are we working on, {name.split(" ")[0]}?
78
- </h1>
79
- <p style={{ color: "rgba(45,74,62,.7)", maxWidth: 610, lineHeight: 1.65 }}>
80
- Start talking immediately. Link projects when you want MOP-AGENT to remember their state and work across them.
81
- </p>
82
- <div className="mop-prompt-grid" style={promptGrid}>
83
- {["Help me plan today’s work", "What can MOP-AGENT do?", "Summarize what you remember", "Plan a new software project"].map((prompt) => (
84
- <button key={prompt} onClick={() => send(prompt)} style={promptCard}>{prompt}<span>→</span></button>
128
+ <div className="mop-assistant-workspace">
129
+ <div className="mop-assistant-conversation">
130
+ {turns.length === 0 ? (
131
+ <div className="mop-assistant-welcome">
132
+ <div style={assistantLogo}><img src="/icon.svg" alt="MOP-AGENT" /></div>
133
+ <p style={{ color: "#742220", fontSize: 11, fontWeight: 900, letterSpacing: ".16em" }}>MOP-AGENT IS READY</p>
134
+ <h1 style={{ fontFamily: '"SFMono-Regular", Consolas, monospace', fontSize: "clamp(26px, 4vw, 40px)", margin: "8px 0 12px" }}>
135
+ What are we working on, {name.split(" ")[0]}?
136
+ </h1>
137
+ <p style={{ color: "rgba(45,74,62,.7)", maxWidth: 610, lineHeight: 1.65 }}>
138
+ Start talking immediately. Link projects when you want MOP-AGENT to remember their state and work across them.
139
+ </p>
140
+ <div className="mop-prompt-grid" style={promptGrid}>
141
+ {["Help me plan today’s work", "What can MOP-AGENT do?", "Summarize what you remember", "Plan a new software project"].map((prompt) => (
142
+ <button key={prompt} onClick={() => send(prompt)} style={promptCard}>{prompt}<span>→</span></button>
143
+ ))}
144
+ </div>
145
+ {projects.length === 0 && (
146
+ <p style={{ fontSize: 13, color: "rgba(45,74,62,.68)", marginTop: 24 }}>
147
+ No project linked yet—this does not block chat. <a href="/brain" style={{ color: "#742220" }}>Link one from Brain →</a>
148
+ </p>
149
+ )}
150
+ </div>
151
+ ) : (
152
+ <div style={{ width: "min(100%, 820px)", margin: "0 auto", padding: "28px 0 160px" }}>
153
+ {turns.map((turn, index) => (
154
+ <article key={index} style={{ display: "grid", gridTemplateColumns: "34px 1fr", gap: 13, marginBottom: 26 }}>
155
+ <span style={turn.role === "assistant" ? botAvatar : userAvatar}>{turn.role === "assistant" ? "✦" : name.slice(0, 1).toUpperCase()}</span>
156
+ <div>
157
+ <strong style={{ fontSize: 13, color: turn.role === "assistant" ? "#742220" : "#2d4a3e" }}>{turn.role === "assistant" ? "MOP-AGENT" : "You"}</strong>
158
+ <div style={{ whiteSpace: "pre-wrap", lineHeight: 1.7, marginTop: 6, color: "#2d4a3e" }}>{turn.content || "Thinking…"}</div>
159
+ </div>
160
+ </article>
85
161
  ))}
162
+ <div ref={endRef} />
86
163
  </div>
87
- {projects.length === 0 && (
88
- <p style={{ fontSize: 13, color: "rgba(45,74,62,.68)", marginTop: 24 }}>
89
- No project linked yet—this does not block chat. <a href="/brain" style={{ color: "#742220" }}>Link one from Brain →</a>
90
- </p>
91
- )}
164
+ )}
165
+ </div>
166
+
167
+ <div className="mop-assistant-composer-wrap">
168
+ <div style={composer}>
169
+ <textarea
170
+ value={input}
171
+ onChange={(e) => setInput(e.target.value)}
172
+ onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }}
173
+ placeholder="Message MOP-AGENT…"
174
+ rows={1}
175
+ style={textarea}
176
+ />
177
+ <button onClick={() => send()} disabled={busy || !input.trim()} style={{ ...sendButton, opacity: busy || !input.trim() ? .45 : 1 }}>↑</button>
92
178
  </div>
93
- ) : (
94
- <div style={{ width: "min(100%, 820px)", margin: "0 auto", padding: "28px 0 160px" }}>
95
- {turns.map((turn, index) => (
96
- <article key={index} style={{ display: "grid", gridTemplateColumns: "34px 1fr", gap: 13, marginBottom: 26 }}>
97
- <span style={turn.role === "assistant" ? botAvatar : userAvatar}>{turn.role === "assistant" ? "✦" : name.slice(0, 1).toUpperCase()}</span>
98
- <div>
99
- <strong style={{ fontSize: 13, color: turn.role === "assistant" ? "#742220" : "#2d4a3e" }}>{turn.role === "assistant" ? "MOP-AGENT" : "You"}</strong>
100
- <div style={{ whiteSpace: "pre-wrap", lineHeight: 1.7, marginTop: 6, color: "#2d4a3e" }}>{turn.content || "Thinking…"}</div>
101
- </div>
102
- </article>
103
- ))}
104
- <div ref={endRef} />
179
+ <div style={{ textAlign: "center", color: "rgba(45,74,62,.62)", fontSize: 11, marginTop: 8 }}>
180
+ {providerUsed ? `Answered by ${providerUsed} · ` : ""}Cross-project memory
105
181
  </div>
106
- )}
182
+ </div>
107
183
  </div>
108
184
 
109
- <div className="mop-assistant-composer-wrap">
110
- <div style={composer}>
111
- <textarea
112
- value={input}
113
- onChange={(e) => setInput(e.target.value)}
114
- onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }}
115
- placeholder="Message MOP-AGENT…"
116
- rows={1}
117
- style={textarea}
118
- />
119
- <button onClick={() => send()} disabled={busy || !input.trim()} style={{ ...sendButton, opacity: busy || !input.trim() ? .45 : 1 }}>↑</button>
185
+ <aside className="mop-chat-history" aria-label="Chat history">
186
+ <div className="mop-chat-history-header">
187
+ <div>
188
+ <span>MEMORY LOG</span>
189
+ <strong>Chat history</strong>
190
+ </div>
191
+ <button type="button" onClick={startNewChat} disabled={busy} title="Start a new chat">+</button>
120
192
  </div>
121
- <div style={{ textAlign: "center", color: "rgba(45,74,62,.62)", fontSize: 11, marginTop: 8 }}>
122
- {providerUsed ? `Answered by ${providerUsed} · ` : ""}Cross-project memory
193
+ <div className="mop-chat-history-list">
194
+ {history.length === 0 ? (
195
+ <p className="mop-chat-history-empty">Your conversations will appear here.</p>
196
+ ) : history.map((chat) => (
197
+ <div className={`mop-chat-history-item${chat.id === activeChatId ? " is-active" : ""}`} key={chat.id}>
198
+ <button type="button" className="mop-chat-history-open" onClick={() => openChat(chat)} disabled={busy}>
199
+ <strong>{chat.title}</strong>
200
+ <span>{formatChatDate(chat.updatedAt)}</span>
201
+ </button>
202
+ <button type="button" className="mop-chat-history-delete" onClick={() => deleteChat(chat.id)} disabled={busy} aria-label={`Delete ${chat.title}`}>×</button>
203
+ </div>
204
+ ))}
123
205
  </div>
124
- </div>
206
+ </aside>
125
207
  </section>
126
208
  );
127
209
  }
128
210
 
211
+ function formatChatDate(timestamp: number) {
212
+ const date = new Date(timestamp);
213
+ if (Number.isNaN(date.getTime())) return "Saved conversation";
214
+ return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }).format(date);
215
+ }
216
+
129
217
  const assistantLogo: CSSProperties = { width: 86, height: 86, display: "grid", placeItems: "center" };
130
218
  const promptGrid: CSSProperties = { width: "min(100%, 650px)", display: "grid", gridTemplateColumns: "repeat(2,minmax(0,1fr))", gap: 10, marginTop: 28 };
131
219
  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" };
@@ -272,31 +272,7 @@ button {
272
272
  font-size: 16px;
273
273
  }
274
274
 
275
- .mop-project-memory {
276
- min-height: 0;
277
- overflow: hidden;
278
- }
279
-
280
- .mop-project-memory nav {
281
- max-height: min(34vh, 280px);
282
- overflow-y: auto;
283
- scrollbar-width: thin;
284
- }
285
-
286
- .mop-project-memory a { min-height: 34px; padding-block: 5px; font-size: 13px; }
287
- .mop-project-dot {
288
- flex: 0 0 auto;
289
- width: 7px;
290
- height: 7px;
291
- background: rgba(254, 249, 225, .34);
292
- }
293
- .mop-project-dot[data-online="true"] { background: #78e19b; box-shadow: 0 0 0 2px rgba(120, 225, 155, .12); }
294
- .mop-nav-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
295
- .mop-sidebar-empty { display: block; padding: 7px 11px; color: rgba(254, 249, 225, .42); font-size: 12px; }
296
275
  .mop-admin-nav { margin-top: 2px; }
297
- .mop-settings-subnav { display: grid; gap: 1px; margin: 1px 0 2px 31px; border-left: 1px solid rgba(254, 249, 225, .18); }
298
- .mop-settings-subnav button { min-height: 32px; padding: 5px 12px; color: rgba(254, 249, 225, .6); font-size: 12px; }
299
- .mop-settings-subnav button.is-active { color: #ff9a56; border-color: transparent; background: transparent; box-shadow: none; }
300
276
 
301
277
  .mop-sidebar-spacer { flex: 1; }
302
278
  .mop-account-card {
@@ -389,7 +365,17 @@ button {
389
365
 
390
366
  /* Assistant content now lives inside the shared shell. */
391
367
  .mop-assistant-page {
392
- min-height: calc(100vh - 70px);
368
+ height: calc(100vh - 70px);
369
+ min-height: 560px;
370
+ position: relative;
371
+ display: grid;
372
+ grid-template-columns: minmax(0, 1fr) 290px;
373
+ overflow: hidden;
374
+ }
375
+
376
+ .mop-assistant-workspace {
377
+ min-width: 0;
378
+ min-height: 0;
393
379
  position: relative;
394
380
  display: flex;
395
381
  flex-direction: column;
@@ -417,6 +403,104 @@ button {
417
403
  background: linear-gradient(transparent, var(--mop-cream) 28%);
418
404
  }
419
405
 
406
+ .mop-chat-history {
407
+ min-width: 0;
408
+ min-height: 0;
409
+ display: flex;
410
+ flex-direction: column;
411
+ padding: 17px 13px 13px;
412
+ color: var(--mop-green);
413
+ border-left: 1px solid rgba(45, 74, 62, .3);
414
+ background:
415
+ linear-gradient(rgba(255, 253, 242, .84), rgba(254, 249, 225, .94)),
416
+ repeating-linear-gradient(0deg, transparent 0, transparent 7px, rgba(116, 34, 32, .04) 7px, rgba(116, 34, 32, .04) 8px);
417
+ }
418
+
419
+ .mop-chat-history-header {
420
+ display: flex;
421
+ align-items: center;
422
+ justify-content: space-between;
423
+ gap: 12px;
424
+ padding: 2px 2px 14px;
425
+ border-bottom: 1px solid rgba(45, 74, 62, .24);
426
+ }
427
+
428
+ .mop-chat-history-header > div { display: grid; gap: 3px; }
429
+ .mop-chat-history-header span {
430
+ color: var(--mop-red);
431
+ font-family: "SFMono-Regular", Consolas, monospace;
432
+ font-size: 8px;
433
+ font-weight: 900;
434
+ letter-spacing: .16em;
435
+ }
436
+ .mop-chat-history-header strong { font-family: "SFMono-Regular", Consolas, monospace; font-size: 15px; }
437
+ .mop-chat-history-header button {
438
+ width: 34px;
439
+ height: 34px;
440
+ border: 1px solid rgba(45, 74, 62, .38);
441
+ background: var(--mop-red);
442
+ color: var(--mop-cream);
443
+ font-size: 20px;
444
+ cursor: pointer;
445
+ }
446
+
447
+ .mop-chat-history-list {
448
+ min-height: 0;
449
+ display: grid;
450
+ align-content: start;
451
+ gap: 6px;
452
+ overflow-y: auto;
453
+ padding: 12px 1px;
454
+ scrollbar-width: thin;
455
+ }
456
+
457
+ .mop-chat-history-empty {
458
+ margin: 8px 5px;
459
+ color: rgba(45, 74, 62, .58);
460
+ font-size: 12px;
461
+ line-height: 1.55;
462
+ }
463
+
464
+ .mop-chat-history-item {
465
+ display: grid;
466
+ grid-template-columns: minmax(0, 1fr) 28px;
467
+ align-items: stretch;
468
+ border: 1px solid transparent;
469
+ }
470
+ .mop-chat-history-item:hover,
471
+ .mop-chat-history-item.is-active {
472
+ border-color: rgba(45, 74, 62, .24);
473
+ background: rgba(255, 253, 242, .78);
474
+ }
475
+ .mop-chat-history-item.is-active { border-left: 3px solid var(--mop-red); }
476
+ .mop-chat-history-open,
477
+ .mop-chat-history-delete {
478
+ min-width: 0;
479
+ border: 0;
480
+ box-shadow: none;
481
+ background: transparent;
482
+ color: var(--mop-green);
483
+ cursor: pointer;
484
+ }
485
+ .mop-chat-history-open {
486
+ display: grid;
487
+ gap: 4px;
488
+ padding: 9px 7px 9px 9px;
489
+ text-align: left;
490
+ }
491
+ .mop-chat-history-open strong {
492
+ overflow: hidden;
493
+ text-overflow: ellipsis;
494
+ white-space: nowrap;
495
+ font-size: 12px;
496
+ font-weight: 760;
497
+ }
498
+ .mop-chat-history-open span { color: rgba(45, 74, 62, .5); font-size: 9px; }
499
+ .mop-chat-history-delete { opacity: 0; padding: 0; font-size: 17px; }
500
+ .mop-chat-history-item:hover .mop-chat-history-delete,
501
+ .mop-chat-history-item.is-active .mop-chat-history-delete { opacity: .65; }
502
+ .mop-chat-history-delete:hover { color: var(--mop-red); opacity: 1 !important; }
503
+
420
504
  .mop-settings-grid {
421
505
  display: grid;
422
506
  grid-template-columns: 1fr;
@@ -484,9 +568,35 @@ button {
484
568
  background: rgba(20, 34, 29, .56);
485
569
  }
486
570
  .mop-app-main { min-height: calc(100vh - 62px); }
487
- .mop-assistant-page { min-height: calc(100vh - 62px); }
571
+ .mop-assistant-page { height: calc(100vh - 62px); min-height: 520px; grid-template-columns: minmax(0, 1fr); }
572
+ .mop-chat-history { display: none; }
488
573
  .mop-assistant-conversation { padding: 0 16px; }
489
574
  .mop-assistant-composer-wrap { padding: 26px 12px 12px; }
490
575
  .mop-settings-grid { grid-template-columns: 1fr; }
491
576
  .mop-user-invite-form { grid-template-columns: 1fr !important; }
492
577
  }
578
+
579
+ .mop-back-workspace-btn {
580
+ display: flex;
581
+ align-items: center;
582
+ justify-content: center;
583
+ width: 100%;
584
+ min-height: 40px;
585
+ margin-bottom: 9px;
586
+ padding: 9px 12px;
587
+ border: 1px solid var(--mop-red);
588
+ background: var(--mop-red);
589
+ color: var(--mop-cream);
590
+ font-family: "SFMono-Regular", Consolas, monospace;
591
+ font-size: 11px;
592
+ font-weight: 900;
593
+ text-decoration: none;
594
+ box-shadow: 2px 2px 0 rgba(18, 38, 30, .28);
595
+ }
596
+
597
+ .mop-back-workspace-btn:hover { color: var(--mop-cream); transform: translate(-1px, -1px); }
598
+
599
+ @media (min-width: 761px) and (max-width: 1050px) {
600
+ .mop-assistant-page { grid-template-columns: minmax(0, 1fr) 235px; }
601
+ .mop-chat-history { padding-inline: 9px; }
602
+ }
@@ -94,53 +94,53 @@ export function AppShell({ viewer, children }: { viewer: AppViewer; children: Re
94
94
  {menuOpen && <button className="mop-sidebar-scrim" aria-label="Close navigation" onClick={() => setMenuOpen(false)} />}
95
95
 
96
96
  <aside className={`mop-app-sidebar${menuOpen ? " is-open" : ""}`}>
97
- <nav className="mop-sidebar-primary" aria-label="Workspace">
98
- <a href="/assistant" className={pathname.startsWith("/assistant") || pathname.startsWith("/chat/") ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
99
- <span className="mop-nav-icon">✎</span>
100
- <span>New chat</span>
101
- </a>
102
- <a href="/brain" className={pathname.startsWith("/brain") ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
103
- <span className="mop-nav-icon">◉</span>
104
- <span>Brain</span>
105
- </a>
106
- </nav>
107
-
108
- <div className="mop-nav-section mop-project-memory">
109
- <p>PROJECT MEMORY</p>
110
- <nav>
111
- {projects.slice(0, 8).map((project) => (
112
- <a key={project.id} href={`/brain/${project.id}`} className={pathname === `/brain/${project.id}` ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
113
- <span className="mop-project-dot" data-online={project.status === "online"} />
114
- <span className="mop-nav-label">{project.name}</span>
115
- </a>
116
- ))}
117
- {projects.length === 0 && <span className="mop-sidebar-empty">No linked projects yet</span>}
118
- </nav>
119
- </div>
120
-
121
- {isAdmin && (
122
- <div className="mop-nav-section mop-admin-nav">
123
- <p>ADMIN</p>
97
+ {isSettings ? (
98
+ <div className="mop-nav-section">
99
+ <p>SETTINGS</p>
124
100
  <nav>
125
- <a href="/settings" className={isSettings ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
126
- <span className="mop-nav-icon">⚙</span>
127
- <span>Settings</span>
128
- </a>
129
- {isSettings && (
130
- <div className="mop-settings-subnav">
131
- <button className={settingsSection === "providers" ? "is-active" : ""} onClick={() => { selectSection("providers"); setMenuOpen(false); }}>
132
- <span>Providers</span>
133
- </button>
134
- <button className={settingsSection === "users" ? "is-active" : ""} onClick={() => { selectSection("users"); setMenuOpen(false); }}>
135
- <span>Users</span>
136
- </button>
137
- </div>
138
- )}
101
+ <button className={settingsSection === "providers" ? "is-active" : ""} onClick={() => { selectSection("providers"); setMenuOpen(false); }}>
102
+ <span className="mop-nav-icon">◇</span>
103
+ <span>Providers</span>
104
+ </button>
105
+ <button className={settingsSection === "users" ? "is-active" : ""} onClick={() => { selectSection("users"); setMenuOpen(false); }}>
106
+ <span className="mop-nav-icon">♙</span>
107
+ <span>Users</span>
108
+ </button>
139
109
  </nav>
140
110
  </div>
111
+ ) : (
112
+ <>
113
+ <nav className="mop-sidebar-primary" aria-label="Workspace">
114
+ <a href="/assistant" className={pathname.startsWith("/assistant") || pathname.startsWith("/chat/") ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
115
+ <span className="mop-nav-icon">✎</span>
116
+ <span>New chat</span>
117
+ </a>
118
+ <a href="/brain" className={pathname.startsWith("/brain") ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
119
+ <span className="mop-nav-icon">◉</span>
120
+ <span>Brain</span>
121
+ </a>
122
+ </nav>
123
+
124
+ {isAdmin && (
125
+ <div className="mop-nav-section mop-admin-nav">
126
+ <p>ADMIN</p>
127
+ <nav>
128
+ <a href="/settings" onClick={() => setMenuOpen(false)}>
129
+ <span className="mop-nav-icon">⚙</span>
130
+ <span>Settings</span>
131
+ </a>
132
+ </nav>
133
+ </div>
134
+ )}
135
+ </>
141
136
  )}
142
137
 
143
138
  <div className="mop-sidebar-spacer" />
139
+ {isSettings && (
140
+ <a href="/assistant" className="mop-back-workspace-btn" onClick={() => setMenuOpen(false)}>
141
+ <span>← BACK TO WORKSPACE</span>
142
+ </a>
143
+ )}
144
144
  <button className="mop-account-card" type="button" onClick={logout} title="Sign out">
145
145
  <span className="mop-account-avatar">{viewer.name.slice(0, 1).toUpperCase()}</span>
146
146
  <span className="mop-account-copy">
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "mop-agent",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "mop-agent",
9
- "version": "0.1.13",
9
+ "version": "0.1.14",
10
10
  "license": "UNLICENSED",
11
11
  "workspaces": [
12
12
  "packages/*",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mop-agent",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Self-hosted AI assistant with persistent cross-project memory, installed with npx mop-agent.",
5
5
  "author": "BURHANDEV ENTERPRISE",
6
6
  "license": "UNLICENSED",