mop-agent 0.1.13 → 0.1.15

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.15` 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,10 @@ 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("");
22
+ const [historyCollapsed, setHistoryCollapsed] = useState(false);
16
23
  const endRef = useRef<HTMLDivElement>(null);
17
24
 
18
25
  useEffect(() => {
@@ -21,10 +28,69 @@ export default function AssistantPage() {
21
28
  }).catch(() => {});
22
29
  }, []);
23
30
 
31
+ useEffect(() => {
32
+ try {
33
+ const stored = JSON.parse(window.localStorage.getItem(CHAT_HISTORY_KEY) ?? "[]");
34
+ if (Array.isArray(stored)) setHistory(stored.slice(0, 30));
35
+ setHistoryCollapsed(window.localStorage.getItem("mop-agent-history-collapsed") === "1");
36
+ } catch {
37
+ window.localStorage.removeItem(CHAT_HISTORY_KEY);
38
+ } finally {
39
+ setHistoryReady(true);
40
+ }
41
+ }, []);
42
+
43
+ useEffect(() => {
44
+ if (!historyReady) return;
45
+ window.localStorage.setItem(CHAT_HISTORY_KEY, JSON.stringify(history.slice(0, 30)));
46
+ }, [history, historyReady]);
47
+
48
+ useEffect(() => {
49
+ if (!activeChatId || turns.length === 0) return;
50
+ const firstMessage = turns.find((turn) => turn.role === "user")?.content ?? "New conversation";
51
+ setHistory((current) => {
52
+ const updated: SavedChat = {
53
+ id: activeChatId,
54
+ title: firstMessage.slice(0, 54),
55
+ turns,
56
+ updatedAt: Date.now(),
57
+ };
58
+ return [updated, ...current.filter((chat) => chat.id !== activeChatId)].slice(0, 30);
59
+ });
60
+ }, [activeChatId, turns]);
61
+
62
+ function startNewChat() {
63
+ if (busy) return;
64
+ setActiveChatId("");
65
+ setTurns([]);
66
+ setProviderUsed("");
67
+ setInput("");
68
+ }
69
+
70
+ function openChat(chat: SavedChat) {
71
+ if (busy) return;
72
+ setActiveChatId(chat.id);
73
+ setTurns(chat.turns);
74
+ setProviderUsed("");
75
+ }
76
+
77
+ function deleteChat(id: string) {
78
+ if (busy) return;
79
+ setHistory((current) => current.filter((chat) => chat.id !== id));
80
+ if (activeChatId === id) startNewChat();
81
+ }
82
+
83
+ function toggleHistory() {
84
+ setHistoryCollapsed((collapsed) => {
85
+ window.localStorage.setItem("mop-agent-history-collapsed", collapsed ? "0" : "1");
86
+ return !collapsed;
87
+ });
88
+ }
24
89
 
25
90
  async function send(prefill?: string) {
26
91
  const message = (prefill ?? input).trim();
27
92
  if (!message || busy) return;
93
+ if (!activeChatId) setActiveChatId(`chat-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
28
94
  setInput("");
29
95
  setBusy(true);
30
96
  setTurns((current) => [...current, { role: "user", content: message }, { role: "assistant", content: "" }]);
@@ -67,65 +133,105 @@ export default function AssistantPage() {
67
133
  }
68
134
 
69
135
  return (
70
- <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>
136
+ <section className={`mop-assistant-page${historyCollapsed ? " is-history-collapsed" : ""}`}>
137
+ <div className="mop-assistant-workspace">
138
+ <div className="mop-assistant-conversation">
139
+ {turns.length === 0 ? (
140
+ <div className="mop-assistant-welcome">
141
+ <div style={assistantLogo}><img src="/icon.svg" alt="MOP-AGENT" /></div>
142
+ <p style={{ color: "#742220", fontSize: 11, fontWeight: 900, letterSpacing: ".16em" }}>MOP-AGENT IS READY</p>
143
+ <h1 style={{ fontFamily: '"SFMono-Regular", Consolas, monospace', fontSize: "clamp(26px, 4vw, 40px)", margin: "8px 0 12px" }}>
144
+ What are we working on, {name.split(" ")[0]}?
145
+ </h1>
146
+ <p style={{ color: "rgba(45,74,62,.7)", maxWidth: 610, lineHeight: 1.65 }}>
147
+ Start talking immediately. Link projects when you want MOP-AGENT to remember their state and work across them.
148
+ </p>
149
+ <div className="mop-prompt-grid" style={promptGrid}>
150
+ {["Help me plan today’s work", "What can MOP-AGENT do?", "Summarize what you remember", "Plan a new software project"].map((prompt) => (
151
+ <button key={prompt} onClick={() => send(prompt)} style={promptCard}>{prompt}<span>→</span></button>
152
+ ))}
153
+ </div>
154
+ {projects.length === 0 && (
155
+ <p style={{ fontSize: 13, color: "rgba(45,74,62,.68)", marginTop: 24 }}>
156
+ No project linked yet—this does not block chat. <a href="/brain" style={{ color: "#742220" }}>Link one from Brain →</a>
157
+ </p>
158
+ )}
159
+ </div>
160
+ ) : (
161
+ <div style={{ width: "min(100%, 820px)", margin: "0 auto", padding: "28px 0 160px" }}>
162
+ {turns.map((turn, index) => (
163
+ <article key={index} style={{ display: "grid", gridTemplateColumns: "34px 1fr", gap: 13, marginBottom: 26 }}>
164
+ <span style={turn.role === "assistant" ? botAvatar : userAvatar}>{turn.role === "assistant" ? "✦" : name.slice(0, 1).toUpperCase()}</span>
165
+ <div>
166
+ <strong style={{ fontSize: 13, color: turn.role === "assistant" ? "#742220" : "#2d4a3e" }}>{turn.role === "assistant" ? "MOP-AGENT" : "You"}</strong>
167
+ <div style={{ whiteSpace: "pre-wrap", lineHeight: 1.7, marginTop: 6, color: "#2d4a3e" }}>{turn.content || "Thinking…"}</div>
168
+ </div>
169
+ </article>
85
170
  ))}
171
+ <div ref={endRef} />
86
172
  </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
- )}
173
+ )}
174
+ </div>
175
+
176
+ <div className="mop-assistant-composer-wrap">
177
+ <div style={composer}>
178
+ <textarea
179
+ value={input}
180
+ onChange={(e) => setInput(e.target.value)}
181
+ onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }}
182
+ placeholder="Message MOP-AGENT…"
183
+ rows={1}
184
+ style={textarea}
185
+ />
186
+ <button onClick={() => send()} disabled={busy || !input.trim()} style={{ ...sendButton, opacity: busy || !input.trim() ? .45 : 1 }}>↑</button>
92
187
  </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} />
188
+ <div style={{ textAlign: "center", color: "rgba(45,74,62,.62)", fontSize: 11, marginTop: 8 }}>
189
+ {providerUsed ? `Answered by ${providerUsed} · ` : ""}Cross-project memory
105
190
  </div>
106
- )}
191
+ </div>
107
192
  </div>
108
193
 
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>
194
+ <aside className="mop-chat-history" aria-label="Chat history">
195
+ <div className="mop-chat-history-header">
196
+ <strong>Chat history</strong>
197
+ <div className="mop-chat-history-actions">
198
+ <button
199
+ className="mop-chat-history-collapse"
200
+ type="button"
201
+ onClick={toggleHistory}
202
+ aria-label={historyCollapsed ? "Expand chat history" : "Collapse chat history"}
203
+ aria-expanded={!historyCollapsed}
204
+ title={historyCollapsed ? "Expand chat history" : "Collapse chat history"}
205
+ >
206
+ {historyCollapsed ? "‹" : "›"}
207
+ </button>
208
+ <button className="mop-chat-history-new" type="button" onClick={startNewChat} disabled={busy} title="Start a new chat">+</button>
209
+ </div>
120
210
  </div>
121
- <div style={{ textAlign: "center", color: "rgba(45,74,62,.62)", fontSize: 11, marginTop: 8 }}>
122
- {providerUsed ? `Answered by ${providerUsed} · ` : ""}Cross-project memory
211
+ <div className="mop-chat-history-list">
212
+ {history.length === 0 ? (
213
+ <p className="mop-chat-history-empty">Your conversations will appear here.</p>
214
+ ) : history.map((chat) => (
215
+ <div className={`mop-chat-history-item${chat.id === activeChatId ? " is-active" : ""}`} key={chat.id}>
216
+ <button type="button" className="mop-chat-history-open" onClick={() => openChat(chat)} disabled={busy}>
217
+ <strong>{chat.title}</strong>
218
+ <span>{formatChatDate(chat.updatedAt)}</span>
219
+ </button>
220
+ <button type="button" className="mop-chat-history-delete" onClick={() => deleteChat(chat.id)} disabled={busy} aria-label={`Delete ${chat.title}`}>×</button>
221
+ </div>
222
+ ))}
123
223
  </div>
124
- </div>
224
+ </aside>
125
225
  </section>
126
226
  );
127
227
  }
128
228
 
229
+ function formatChatDate(timestamp: number) {
230
+ const date = new Date(timestamp);
231
+ if (Number.isNaN(date.getTime())) return "Saved conversation";
232
+ return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }).format(date);
233
+ }
234
+
129
235
  const assistantLogo: CSSProperties = { width: 86, height: 86, display: "grid", placeItems: "center" };
130
236
  const promptGrid: CSSProperties = { width: "min(100%, 650px)", display: "grid", gridTemplateColumns: "repeat(2,minmax(0,1fr))", gap: 10, marginTop: 28 };
131
237
  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" };
@@ -121,15 +121,22 @@ button {
121
121
  box-shadow: 0 2px 0 rgba(45, 74, 62, .28);
122
122
  }
123
123
 
124
+ .mop-app-brand-cell {
125
+ position: relative;
126
+ min-width: 0;
127
+ display: flex;
128
+ border-right: 1px solid rgba(254, 249, 225, .16);
129
+ }
130
+
124
131
  .mop-app-brand {
132
+ width: 100%;
125
133
  display: flex;
126
134
  align-items: center;
127
135
  gap: 9px;
128
136
  min-width: 0;
129
- padding: 7px 14px;
137
+ padding: 7px 46px 7px 14px;
130
138
  color: var(--mop-cream);
131
139
  text-decoration: none;
132
- border-right: 1px solid rgba(254, 249, 225, .16);
133
140
  }
134
141
 
135
142
  .mop-app-brand img {
@@ -148,6 +155,51 @@ button {
148
155
  white-space: nowrap;
149
156
  }
150
157
 
158
+ .mop-sidebar-collapse-toggle {
159
+ position: absolute;
160
+ top: 50%;
161
+ right: 9px;
162
+ z-index: 2;
163
+ width: 29px;
164
+ height: 34px;
165
+ display: grid;
166
+ place-items: center;
167
+ padding: 0;
168
+ transform: translateY(-50%);
169
+ border: 1px solid rgba(254, 249, 225, .25);
170
+ background: rgba(254, 249, 225, .08);
171
+ color: var(--mop-cream);
172
+ font-size: 19px;
173
+ cursor: pointer;
174
+ }
175
+
176
+ .mop-sidebar-collapse-toggle:hover:not(:disabled) { transform: translate(-1px, calc(-50% - 1px)); }
177
+ .mop-sidebar-collapse-toggle:active:not(:disabled) { transform: translate(1px, calc(-50% + 1px)); }
178
+
179
+ .mop-app-frame,
180
+ .mop-app-topbar { transition: grid-template-columns 140ms steps(4, end); }
181
+
182
+ .mop-app-frame.is-sidebar-collapsed { grid-template-columns: 74px minmax(0, 1fr); }
183
+ .mop-app-frame.is-sidebar-collapsed .mop-app-topbar { grid-template-columns: 74px minmax(0, 1fr); }
184
+ .mop-app-frame.is-sidebar-collapsed .mop-app-brand { justify-content: center; padding: 7px; }
185
+ .mop-app-frame.is-sidebar-collapsed .mop-app-brand img { width: 47px; height: 47px; }
186
+ .mop-app-frame.is-sidebar-collapsed .mop-app-brand span,
187
+ .mop-app-frame.is-sidebar-collapsed .mop-nav-section > p,
188
+ .mop-app-frame.is-sidebar-collapsed .mop-sidebar-primary a > span:not(.mop-nav-icon),
189
+ .mop-app-frame.is-sidebar-collapsed .mop-nav-section a > span:not(.mop-nav-icon),
190
+ .mop-app-frame.is-sidebar-collapsed .mop-nav-section button > span:not(.mop-nav-icon),
191
+ .mop-app-frame.is-sidebar-collapsed .mop-account-copy,
192
+ .mop-app-frame.is-sidebar-collapsed .mop-account-card > span:last-child,
193
+ .mop-app-frame.is-sidebar-collapsed .mop-back-workspace-btn > span { display: none; }
194
+ .mop-app-frame.is-sidebar-collapsed .mop-sidebar-collapse-toggle { right: -14px; }
195
+ .mop-app-frame.is-sidebar-collapsed .mop-app-sidebar { padding-inline: 9px; }
196
+ .mop-app-frame.is-sidebar-collapsed .mop-sidebar-primary a,
197
+ .mop-app-frame.is-sidebar-collapsed .mop-nav-section a,
198
+ .mop-app-frame.is-sidebar-collapsed .mop-nav-section button,
199
+ .mop-app-frame.is-sidebar-collapsed .mop-account-card { justify-content: center; padding-inline: 5px; }
200
+ .mop-app-frame.is-sidebar-collapsed .mop-nav-icon { width: auto; }
201
+ .mop-app-frame.is-sidebar-collapsed .mop-back-workspace-btn::before { content: "←"; }
202
+
151
203
  .mop-app-topbar-main {
152
204
  position: relative;
153
205
  min-width: 0;
@@ -272,31 +324,7 @@ button {
272
324
  font-size: 16px;
273
325
  }
274
326
 
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
327
  .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
328
 
301
329
  .mop-sidebar-spacer { flex: 1; }
302
330
  .mop-account-card {
@@ -389,7 +417,20 @@ button {
389
417
 
390
418
  /* Assistant content now lives inside the shared shell. */
391
419
  .mop-assistant-page {
392
- min-height: calc(100vh - 70px);
420
+ height: calc(100vh - 70px);
421
+ min-height: 560px;
422
+ position: relative;
423
+ display: grid;
424
+ grid-template-columns: minmax(0, 1fr) 290px;
425
+ overflow: hidden;
426
+ transition: grid-template-columns 140ms steps(4, end);
427
+ }
428
+
429
+ .mop-assistant-page.is-history-collapsed { grid-template-columns: minmax(0, 1fr) 52px; }
430
+
431
+ .mop-assistant-workspace {
432
+ min-width: 0;
433
+ min-height: 0;
393
434
  position: relative;
394
435
  display: flex;
395
436
  flex-direction: column;
@@ -417,6 +458,110 @@ button {
417
458
  background: linear-gradient(transparent, var(--mop-cream) 28%);
418
459
  }
419
460
 
461
+ .mop-chat-history {
462
+ min-width: 0;
463
+ min-height: 0;
464
+ display: flex;
465
+ flex-direction: column;
466
+ padding: 17px 13px 13px;
467
+ color: var(--mop-green);
468
+ border-left: 1px solid rgba(45, 74, 62, .3);
469
+ background:
470
+ linear-gradient(rgba(255, 253, 242, .84), rgba(254, 249, 225, .94)),
471
+ repeating-linear-gradient(0deg, transparent 0, transparent 7px, rgba(116, 34, 32, .04) 7px, rgba(116, 34, 32, .04) 8px);
472
+ }
473
+
474
+ .mop-chat-history-header {
475
+ display: flex;
476
+ align-items: center;
477
+ justify-content: space-between;
478
+ gap: 12px;
479
+ padding: 2px 2px 14px;
480
+ border-bottom: 1px solid rgba(45, 74, 62, .24);
481
+ }
482
+
483
+ .mop-chat-history-header strong { font-family: "SFMono-Regular", Consolas, monospace; font-size: 15px; }
484
+ .mop-chat-history-actions { display: flex; gap: 6px; }
485
+ .mop-chat-history-header button {
486
+ width: 34px;
487
+ height: 34px;
488
+ border: 1px solid rgba(45, 74, 62, .38);
489
+ background: var(--mop-red);
490
+ color: var(--mop-cream);
491
+ font-size: 20px;
492
+ cursor: pointer;
493
+ }
494
+ .mop-chat-history-collapse { background: var(--mop-green) !important; }
495
+ .mop-chat-history-new { background: var(--mop-red) !important; }
496
+
497
+ .mop-assistant-page.is-history-collapsed .mop-chat-history { padding: 12px 8px; }
498
+ .mop-assistant-page.is-history-collapsed .mop-chat-history-header {
499
+ justify-content: center;
500
+ padding: 0 0 12px;
501
+ border-bottom: 0;
502
+ }
503
+ .mop-assistant-page.is-history-collapsed .mop-chat-history-header > strong,
504
+ .mop-assistant-page.is-history-collapsed .mop-chat-history-new,
505
+ .mop-assistant-page.is-history-collapsed .mop-chat-history-list { display: none; }
506
+ .mop-assistant-page.is-history-collapsed .mop-chat-history-actions { display: block; }
507
+
508
+ .mop-chat-history-list {
509
+ min-height: 0;
510
+ display: grid;
511
+ align-content: start;
512
+ gap: 6px;
513
+ overflow-y: auto;
514
+ padding: 12px 1px;
515
+ scrollbar-width: thin;
516
+ }
517
+
518
+ .mop-chat-history-empty {
519
+ margin: 8px 5px;
520
+ color: rgba(45, 74, 62, .58);
521
+ font-size: 12px;
522
+ line-height: 1.55;
523
+ }
524
+
525
+ .mop-chat-history-item {
526
+ display: grid;
527
+ grid-template-columns: minmax(0, 1fr) 28px;
528
+ align-items: stretch;
529
+ border: 1px solid transparent;
530
+ }
531
+ .mop-chat-history-item:hover,
532
+ .mop-chat-history-item.is-active {
533
+ border-color: rgba(45, 74, 62, .24);
534
+ background: rgba(255, 253, 242, .78);
535
+ }
536
+ .mop-chat-history-item.is-active { border-left: 3px solid var(--mop-red); }
537
+ .mop-chat-history-open,
538
+ .mop-chat-history-delete {
539
+ min-width: 0;
540
+ border: 0;
541
+ box-shadow: none;
542
+ background: transparent;
543
+ color: var(--mop-green);
544
+ cursor: pointer;
545
+ }
546
+ .mop-chat-history-open {
547
+ display: grid;
548
+ gap: 4px;
549
+ padding: 9px 7px 9px 9px;
550
+ text-align: left;
551
+ }
552
+ .mop-chat-history-open strong {
553
+ overflow: hidden;
554
+ text-overflow: ellipsis;
555
+ white-space: nowrap;
556
+ font-size: 12px;
557
+ font-weight: 760;
558
+ }
559
+ .mop-chat-history-open span { color: rgba(45, 74, 62, .5); font-size: 9px; }
560
+ .mop-chat-history-delete { opacity: 0; padding: 0; font-size: 17px; }
561
+ .mop-chat-history-item:hover .mop-chat-history-delete,
562
+ .mop-chat-history-item.is-active .mop-chat-history-delete { opacity: .65; }
563
+ .mop-chat-history-delete:hover { color: var(--mop-red); opacity: 1 !important; }
564
+
420
565
  .mop-settings-grid {
421
566
  display: grid;
422
567
  grid-template-columns: 1fr;
@@ -457,7 +602,10 @@ button {
457
602
  grid-template-rows: 62px minmax(0, 1fr);
458
603
  grid-template-areas: "topbar" "main";
459
604
  }
605
+ .mop-app-frame.is-sidebar-collapsed { grid-template-columns: 1fr; }
460
606
  .mop-app-topbar { grid-template-columns: 66px minmax(0, 1fr); }
607
+ .mop-app-frame.is-sidebar-collapsed .mop-app-topbar { grid-template-columns: 66px minmax(0, 1fr); }
608
+ .mop-sidebar-collapse-toggle { display: none; }
461
609
  .mop-app-brand { justify-content: center; padding: 5px; }
462
610
  .mop-app-brand img { width: 49px; height: 49px; }
463
611
  .mop-app-brand span { display: none; }
@@ -475,6 +623,18 @@ button {
475
623
  transition: transform 160ms steps(4, end);
476
624
  }
477
625
  .mop-app-sidebar.is-open { transform: translateX(0); }
626
+ .mop-app-frame.is-sidebar-collapsed .mop-nav-section > p { display: block; }
627
+ .mop-app-frame.is-sidebar-collapsed .mop-sidebar-primary a > span:not(.mop-nav-icon),
628
+ .mop-app-frame.is-sidebar-collapsed .mop-nav-section a > span:not(.mop-nav-icon),
629
+ .mop-app-frame.is-sidebar-collapsed .mop-nav-section button > span:not(.mop-nav-icon),
630
+ .mop-app-frame.is-sidebar-collapsed .mop-back-workspace-btn > span { display: inline; }
631
+ .mop-app-frame.is-sidebar-collapsed .mop-account-copy { display: grid; }
632
+ .mop-app-frame.is-sidebar-collapsed .mop-account-card > span:last-child { display: inline; }
633
+ .mop-app-frame.is-sidebar-collapsed .mop-sidebar-primary a,
634
+ .mop-app-frame.is-sidebar-collapsed .mop-nav-section a,
635
+ .mop-app-frame.is-sidebar-collapsed .mop-nav-section button,
636
+ .mop-app-frame.is-sidebar-collapsed .mop-account-card { justify-content: flex-start; padding-inline: 11px; }
637
+ .mop-app-frame.is-sidebar-collapsed .mop-back-workspace-btn::before { content: none; }
478
638
  .mop-sidebar-scrim {
479
639
  display: block;
480
640
  position: fixed;
@@ -484,9 +644,36 @@ button {
484
644
  background: rgba(20, 34, 29, .56);
485
645
  }
486
646
  .mop-app-main { min-height: calc(100vh - 62px); }
487
- .mop-assistant-page { min-height: calc(100vh - 62px); }
647
+ .mop-assistant-page,
648
+ .mop-assistant-page.is-history-collapsed { height: calc(100vh - 62px); min-height: 520px; grid-template-columns: minmax(0, 1fr); }
649
+ .mop-chat-history { display: none; }
488
650
  .mop-assistant-conversation { padding: 0 16px; }
489
651
  .mop-assistant-composer-wrap { padding: 26px 12px 12px; }
490
652
  .mop-settings-grid { grid-template-columns: 1fr; }
491
653
  .mop-user-invite-form { grid-template-columns: 1fr !important; }
492
654
  }
655
+
656
+ .mop-back-workspace-btn {
657
+ display: flex;
658
+ align-items: center;
659
+ justify-content: center;
660
+ width: 100%;
661
+ min-height: 40px;
662
+ margin-bottom: 9px;
663
+ padding: 9px 12px;
664
+ border: 1px solid var(--mop-red);
665
+ background: var(--mop-red);
666
+ color: var(--mop-cream);
667
+ font-family: "SFMono-Regular", Consolas, monospace;
668
+ font-size: 11px;
669
+ font-weight: 900;
670
+ text-decoration: none;
671
+ box-shadow: 2px 2px 0 rgba(18, 38, 30, .28);
672
+ }
673
+
674
+ .mop-back-workspace-btn:hover { color: var(--mop-cream); transform: translate(-1px, -1px); }
675
+
676
+ @media (min-width: 761px) and (max-width: 1050px) {
677
+ .mop-assistant-page { grid-template-columns: minmax(0, 1fr) 235px; }
678
+ .mop-chat-history { padding-inline: 9px; }
679
+ }
@@ -40,6 +40,7 @@ function pageTitle(pathname: string): string {
40
40
  export function AppShell({ viewer, children }: { viewer: AppViewer; children: ReactNode }) {
41
41
  const pathname = usePathname();
42
42
  const [menuOpen, setMenuOpen] = useState(false);
43
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
43
44
  const [projects, setProjects] = useState<Project[]>([]);
44
45
  const [settingsSection, setSettingsSection] = useState<"providers" | "users">("providers");
45
46
  const isAdmin = viewer.role === "owner";
@@ -54,6 +55,7 @@ export function AppShell({ viewer, children }: { viewer: AppViewer; children: Re
54
55
 
55
56
  const requested = new URLSearchParams(window.location.search).get("section");
56
57
  if (requested === "users") setSettingsSection("users");
58
+ setSidebarCollapsed(window.localStorage.getItem("mop-agent-sidebar-collapsed") === "1");
57
59
  }, []);
58
60
 
59
61
  async function logout() {
@@ -66,14 +68,33 @@ export function AppShell({ viewer, children }: { viewer: AppViewer; children: Re
66
68
  window.history.replaceState(null, "", section === "providers" ? "/settings" : "/settings?section=users");
67
69
  }
68
70
 
71
+ function toggleSidebar() {
72
+ setSidebarCollapsed((collapsed) => {
73
+ window.localStorage.setItem("mop-agent-sidebar-collapsed", collapsed ? "0" : "1");
74
+ return !collapsed;
75
+ });
76
+ }
77
+
69
78
  return (
70
79
  <MemoryCoreContext.Provider value={{ projects, settingsSection, setSettingsSection }}>
71
- <div className="mop-app-frame">
80
+ <div className={`mop-app-frame${sidebarCollapsed ? " is-sidebar-collapsed" : ""}`}>
72
81
  <header className="mop-app-topbar">
73
- <a className="mop-app-brand" href="/assistant" aria-label="MOP-AGENT home">
74
- <img src="/icon.svg" alt="" />
75
- <span>MOP-AGENT</span>
76
- </a>
82
+ <div className="mop-app-brand-cell">
83
+ <a className="mop-app-brand" href="/assistant" aria-label="MOP-AGENT home">
84
+ <img src="/icon.svg" alt="" />
85
+ <span>MOP-AGENT</span>
86
+ </a>
87
+ <button
88
+ className="mop-sidebar-collapse-toggle"
89
+ type="button"
90
+ aria-label={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
91
+ aria-expanded={!sidebarCollapsed}
92
+ title={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
93
+ onClick={toggleSidebar}
94
+ >
95
+ {sidebarCollapsed ? "›" : "‹"}
96
+ </button>
97
+ </div>
77
98
  <div className="mop-app-topbar-main">
78
99
  <button
79
100
  className="mop-menu-toggle"
@@ -94,53 +115,53 @@ export function AppShell({ viewer, children }: { viewer: AppViewer; children: Re
94
115
  {menuOpen && <button className="mop-sidebar-scrim" aria-label="Close navigation" onClick={() => setMenuOpen(false)} />}
95
116
 
96
117
  <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>
118
+ {isSettings ? (
119
+ <div className="mop-nav-section">
120
+ <p>SETTINGS</p>
124
121
  <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
- )}
122
+ <button className={settingsSection === "providers" ? "is-active" : ""} onClick={() => { selectSection("providers"); setMenuOpen(false); }}>
123
+ <span className="mop-nav-icon">◇</span>
124
+ <span>Providers</span>
125
+ </button>
126
+ <button className={settingsSection === "users" ? "is-active" : ""} onClick={() => { selectSection("users"); setMenuOpen(false); }}>
127
+ <span className="mop-nav-icon">♙</span>
128
+ <span>Users</span>
129
+ </button>
139
130
  </nav>
140
131
  </div>
132
+ ) : (
133
+ <>
134
+ <nav className="mop-sidebar-primary" aria-label="Workspace">
135
+ <a href="/assistant" className={pathname.startsWith("/assistant") || pathname.startsWith("/chat/") ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
136
+ <span className="mop-nav-icon">✎</span>
137
+ <span>New chat</span>
138
+ </a>
139
+ <a href="/brain" className={pathname.startsWith("/brain") ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
140
+ <span className="mop-nav-icon">◉</span>
141
+ <span>Brain</span>
142
+ </a>
143
+ </nav>
144
+
145
+ {isAdmin && (
146
+ <div className="mop-nav-section mop-admin-nav">
147
+ <p>ADMIN</p>
148
+ <nav>
149
+ <a href="/settings" onClick={() => setMenuOpen(false)}>
150
+ <span className="mop-nav-icon">⚙</span>
151
+ <span>Settings</span>
152
+ </a>
153
+ </nav>
154
+ </div>
155
+ )}
156
+ </>
141
157
  )}
142
158
 
143
159
  <div className="mop-sidebar-spacer" />
160
+ {isSettings && (
161
+ <a href="/assistant" className="mop-back-workspace-btn" onClick={() => setMenuOpen(false)}>
162
+ <span>← BACK TO WORKSPACE</span>
163
+ </a>
164
+ )}
144
165
  <button className="mop-account-card" type="button" onClick={logout} title="Sign out">
145
166
  <span className="mop-account-avatar">{viewer.name.slice(0, 1).toUpperCase()}</span>
146
167
  <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.15",
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.15",
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.15",
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",