mop-agent 0.1.12 → 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,8 +5,9 @@ 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.10` contains the corrected VPS
9
- > installer, one-time Admin setup/login flow, and shared retro application shell.
8
+ > **Release status:** release candidate `mop-agent@0.1.14` contains the corrected VPS
9
+ > installer, one-time Admin setup/login flow, and simplified shared application shell
10
+ > with centered page titles and ChatGPT-inspired navigation.
10
11
  > The canonical installation command is exactly `npx mop-agent`.
11
12
 
12
13
  ## Current status
@@ -5,14 +5,20 @@ 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
- const { selectedProject, setSelectedProject, projects, provider } = useMemoryCore();
13
+ const { projects } = useMemoryCore();
11
14
  const [turns, setTurns] = useState<Turn[]>([]);
12
15
  const [name, setName] = useState("Admin");
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: "" }]);
@@ -34,8 +91,7 @@ export default function AssistantPage() {
34
91
  headers: { "content-type": "application/json" },
35
92
  body: JSON.stringify({
36
93
  message,
37
- projectId: selectedProject || undefined,
38
- allowCrossProject: !selectedProject,
94
+ allowCrossProject: true,
39
95
  }),
40
96
  });
41
97
 
@@ -69,64 +125,95 @@ export default function AssistantPage() {
69
125
 
70
126
  return (
71
127
  <section className="mop-assistant-page">
72
- <div className="mop-assistant-conversation">
73
- {turns.length === 0 ? (
74
- <div className="mop-assistant-welcome">
75
- <div style={assistantLogo}><img src="/icon.svg" alt="MOP-AGENT" /></div>
76
- <p style={{ color: "#742220", fontSize: 11, fontWeight: 900, letterSpacing: ".16em" }}>MOP-AGENT IS READY</p>
77
- <h1 style={{ fontFamily: '"SFMono-Regular", Consolas, monospace', fontSize: "clamp(26px, 4vw, 40px)", margin: "8px 0 12px" }}>
78
- What are we working on, {name.split(" ")[0]}?
79
- </h1>
80
- <p style={{ color: "rgba(45,74,62,.7)", maxWidth: 610, lineHeight: 1.65 }}>
81
- Start talking immediately. Link projects when you want MOP-AGENT to remember their state and work across them.
82
- </p>
83
- <div className="mop-prompt-grid" style={promptGrid}>
84
- {["Help me plan today’s work", "What can MOP-AGENT do?", "Summarize what you remember", "Plan a new software project"].map((prompt) => (
85
- <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>
86
161
  ))}
162
+ <div ref={endRef} />
87
163
  </div>
88
- {projects.length === 0 && (
89
- <p style={{ fontSize: 13, color: "rgba(45,74,62,.68)", marginTop: 24 }}>
90
- No project linked yet—this does not block chat. <a href="/brain" style={{ color: "#742220" }}>Link one from Brain →</a>
91
- </p>
92
- )}
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>
93
178
  </div>
94
- ) : (
95
- <div style={{ width: "min(100%, 820px)", margin: "0 auto", padding: "28px 0 160px" }}>
96
- {turns.map((turn, index) => (
97
- <article key={index} style={{ display: "grid", gridTemplateColumns: "34px 1fr", gap: 13, marginBottom: 26 }}>
98
- <span style={turn.role === "assistant" ? botAvatar : userAvatar}>{turn.role === "assistant" ? "✦" : name.slice(0, 1).toUpperCase()}</span>
99
- <div>
100
- <strong style={{ fontSize: 13, color: turn.role === "assistant" ? "#742220" : "#2d4a3e" }}>{turn.role === "assistant" ? "MOP-AGENT" : "You"}</strong>
101
- <div style={{ whiteSpace: "pre-wrap", lineHeight: 1.7, marginTop: 6, color: "#2d4a3e" }}>{turn.content || "Thinking…"}</div>
102
- </div>
103
- </article>
104
- ))}
105
- <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
106
181
  </div>
107
- )}
182
+ </div>
108
183
  </div>
109
184
 
110
- <div className="mop-assistant-composer-wrap">
111
- <div style={composer}>
112
- <textarea
113
- value={input}
114
- onChange={(e) => setInput(e.target.value)}
115
- onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }}
116
- placeholder="Message MOP-AGENT…"
117
- rows={1}
118
- style={textarea}
119
- />
120
- <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>
121
192
  </div>
122
- <div style={{ textAlign: "center", color: "rgba(45,74,62,.62)", fontSize: 11, marginTop: 8 }}>
123
- {providerUsed ? `Answered by ${providerUsed} · ` : ""}{selectedProject ? "Selected project memory" : "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
+ ))}
124
205
  </div>
125
- </div>
206
+ </aside>
126
207
  </section>
127
208
  );
128
209
  }
129
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
+
130
217
  const assistantLogo: CSSProperties = { width: 86, height: 86, display: "grid", placeItems: "center" };
131
218
  const promptGrid: CSSProperties = { width: "min(100%, 650px)", display: "grid", gridTemplateColumns: "repeat(2,minmax(0,1fr))", gap: 10, marginTop: 28 };
132
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" };
@@ -97,7 +97,7 @@ button {
97
97
  .mop-app-frame {
98
98
  min-height: 100vh;
99
99
  display: grid;
100
- grid-template-columns: 238px minmax(0, 1fr);
100
+ grid-template-columns: 260px minmax(0, 1fr);
101
101
  grid-template-rows: 70px minmax(0, 1fr);
102
102
  grid-template-areas:
103
103
  "topbar topbar"
@@ -111,7 +111,7 @@ button {
111
111
  top: 0;
112
112
  z-index: 50;
113
113
  display: grid;
114
- grid-template-columns: 238px minmax(0, 1fr);
114
+ grid-template-columns: 260px minmax(0, 1fr);
115
115
  min-width: 0;
116
116
  color: var(--mop-cream);
117
117
  background:
@@ -153,6 +153,7 @@ button {
153
153
  min-width: 0;
154
154
  display: flex;
155
155
  align-items: center;
156
+ justify-content: center;
156
157
  gap: 14px;
157
158
  padding: 0 18px;
158
159
  }
@@ -167,18 +168,8 @@ button {
167
168
  cursor: pointer;
168
169
  }
169
170
 
170
- .mop-topbar-title {
171
- display: flex;
172
- align-items: center;
173
- gap: 8px;
174
- min-width: 130px;
175
- font-family: "SFMono-Regular", Consolas, monospace;
176
- font-size: 13px;
177
- letter-spacing: .08em;
178
- text-transform: uppercase;
179
- }
180
-
181
171
  .mop-live-dot {
172
+ flex: 0 0 auto;
182
173
  width: 8px;
183
174
  height: 8px;
184
175
  background: #78e19b;
@@ -186,37 +177,21 @@ button {
186
177
  }
187
178
 
188
179
  .mop-topbar-center {
189
- position: absolute;
190
- left: 50%;
191
- top: 50%;
192
- transform: translate(-50%, -50%);
193
- min-width: 255px;
180
+ min-width: 240px;
181
+ display: flex;
182
+ align-items: center;
183
+ justify-content: center;
184
+ gap: 10px;
194
185
  padding: 9px 28px;
195
186
  text-align: center;
196
187
  color: rgba(254, 249, 225, .86);
197
188
  border: 1px solid rgba(254, 249, 225, .12);
198
189
  background: rgba(254, 249, 225, .07);
199
190
  font-family: "SFMono-Regular", Consolas, monospace;
200
- font-size: 11px;
201
- font-weight: 800;
202
- letter-spacing: .18em;
203
- }
204
-
205
- .mop-topbar-meta {
206
- display: flex;
207
- align-items: center;
208
- gap: 9px;
209
- margin-left: auto;
210
- font-family: "SFMono-Regular", Consolas, monospace;
211
- font-size: 10px;
191
+ font-size: 12px;
212
192
  font-weight: 800;
213
- letter-spacing: .12em;
214
- }
215
-
216
- .mop-version {
217
- padding: 4px 6px;
218
- border: 1px solid rgba(254, 249, 225, .22);
219
- background: rgba(254, 249, 225, .07);
193
+ letter-spacing: .14em;
194
+ text-transform: uppercase;
220
195
  }
221
196
 
222
197
  .mop-app-sidebar {
@@ -228,60 +203,68 @@ button {
228
203
  display: flex;
229
204
  flex-direction: column;
230
205
  overflow-y: auto;
231
- padding: 15px 9px 12px;
206
+ padding: 10px 9px 9px;
232
207
  color: var(--mop-cream);
233
208
  background:
234
- linear-gradient(rgba(255,255,255,.018), rgba(0,0,0,.05)),
209
+ linear-gradient(rgba(255,255,255,.028), rgba(0,0,0,.035)),
235
210
  var(--mop-green);
236
211
  border-right: 2px solid #20362e;
237
212
  }
238
213
 
239
- .mop-nav-section { margin-bottom: 17px; }
240
- .mop-nav-section > p {
241
- margin: 0 8px 9px;
242
- color: rgba(254, 249, 225, .46);
243
- font-family: "SFMono-Regular", Consolas, monospace;
244
- font-size: 9px;
245
- font-weight: 900;
246
- letter-spacing: .22em;
214
+ .mop-sidebar-primary {
215
+ display: grid;
216
+ gap: 3px;
217
+ margin-bottom: 22px;
247
218
  }
248
219
 
249
- .mop-nav-section nav { display: grid; gap: 4px; }
220
+ .mop-sidebar-primary a,
250
221
  .mop-nav-section a,
251
222
  .mop-nav-section button {
252
223
  display: flex;
253
224
  align-items: center;
254
225
  gap: 11px;
255
226
  width: 100%;
256
- min-height: 43px;
257
- padding: 8px 12px;
258
- color: rgba(254, 249, 225, .78);
227
+ min-height: 40px;
228
+ padding: 8px 11px;
229
+ color: rgba(254, 249, 225, .82);
259
230
  border: 1px solid transparent;
260
231
  background: transparent;
261
232
  text-align: left;
262
233
  text-decoration: none;
263
- font-family: "SFMono-Regular", Consolas, monospace;
264
- font-size: 12px;
265
- font-weight: 800;
266
- letter-spacing: .055em;
267
- text-transform: uppercase;
234
+ font-size: 14px;
235
+ font-weight: 540;
236
+ letter-spacing: 0;
268
237
  cursor: pointer;
269
238
  }
270
239
 
240
+ .mop-sidebar-primary a:hover,
271
241
  .mop-nav-section a:hover,
272
242
  .mop-nav-section button:hover {
273
243
  color: var(--mop-cream);
274
- background: rgba(254, 249, 225, .07);
244
+ background: rgba(254, 249, 225, .075);
275
245
  }
276
246
 
247
+ .mop-sidebar-primary a.is-active,
277
248
  .mop-nav-section a.is-active,
278
249
  .mop-nav-section button.is-active {
279
- color: #ff8a3d;
280
- border-color: rgba(254, 249, 225, .18);
281
- background: rgba(254, 249, 225, .09);
282
- box-shadow: inset 3px 0 #ff6f2c, 2px 2px 0 rgba(18, 38, 30, .25);
250
+ color: var(--mop-cream);
251
+ border-color: rgba(254, 249, 225, .12);
252
+ background: rgba(254, 249, 225, .1);
253
+ box-shadow: 2px 2px 0 rgba(18, 38, 30, .22);
283
254
  }
284
255
 
256
+ .mop-nav-section { margin-bottom: 18px; }
257
+ .mop-nav-section > p {
258
+ margin: 0 11px 7px;
259
+ color: rgba(254, 249, 225, .46);
260
+ font-family: "SFMono-Regular", Consolas, monospace;
261
+ font-size: 10px;
262
+ font-weight: 750;
263
+ letter-spacing: .11em;
264
+ }
265
+
266
+ .mop-nav-section nav { display: grid; gap: 2px; }
267
+
285
268
  .mop-nav-icon {
286
269
  width: 21px;
287
270
  text-align: center;
@@ -289,6 +272,8 @@ button {
289
272
  font-size: 16px;
290
273
  }
291
274
 
275
+ .mop-admin-nav { margin-top: 2px; }
276
+
292
277
  .mop-sidebar-spacer { flex: 1; }
293
278
  .mop-account-card {
294
279
  width: 100%;
@@ -297,11 +282,12 @@ button {
297
282
  gap: 9px;
298
283
  padding: 10px 8px;
299
284
  color: var(--mop-cream);
300
- border: 1px solid rgba(254, 249, 225, .13);
301
- background: rgba(0, 0, 0, .07);
285
+ border: 1px solid transparent;
286
+ background: transparent;
302
287
  text-align: left;
303
288
  cursor: pointer;
304
289
  }
290
+ .mop-account-card:hover { border-color: rgba(254, 249, 225, .1); background: rgba(254, 249, 225, .07); }
305
291
 
306
292
  .mop-account-avatar {
307
293
  flex: 0 0 auto;
@@ -379,69 +365,32 @@ button {
379
365
 
380
366
  /* Assistant content now lives inside the shared shell. */
381
367
  .mop-assistant-page {
382
- min-height: calc(100vh - 70px);
368
+ height: calc(100vh - 70px);
369
+ min-height: 560px;
383
370
  position: relative;
384
- display: flex;
385
- flex-direction: column;
371
+ display: grid;
372
+ grid-template-columns: minmax(0, 1fr) 290px;
373
+ overflow: hidden;
386
374
  }
387
375
 
388
- /* Toolbar lives inside the dark maroon topbar, so its text is light. */
389
- .mop-assistant-toolbar {
390
- width: 100%;
391
- display: flex;
392
- align-items: center;
393
- justify-content: space-between;
394
- gap: 16px;
395
- }
396
- .mop-assistant-status {
397
- display: flex;
398
- align-items: center;
399
- gap: 9px;
376
+ .mop-assistant-workspace {
400
377
  min-width: 0;
401
- }
402
- .mop-assistant-status strong {
403
- font-family: "SFMono-Regular", Consolas, monospace;
404
- font-size: 13px;
405
- letter-spacing: .08em;
406
- text-transform: uppercase;
407
- color: var(--mop-cream);
408
- }
409
- .mop-assistant-provider {
410
- overflow: hidden;
411
- text-overflow: ellipsis;
412
- white-space: nowrap;
413
- color: rgba(254, 249, 225, .66);
414
- font-size: 12px;
415
- }
416
- .mop-assistant-scope {
378
+ min-height: 0;
379
+ position: relative;
417
380
  display: flex;
418
- align-items: center;
419
- gap: 8px;
420
- flex: 0 0 auto;
421
- color: rgba(254, 249, 225, .82);
422
- font-family: "SFMono-Regular", Consolas, monospace;
423
- font-size: 10px;
424
- font-weight: 800;
425
- letter-spacing: .12em;
426
- text-transform: uppercase;
427
- }
428
- .mop-assistant-scope select {
429
- color: var(--mop-green);
430
- background: var(--mop-paper);
431
- border: 1px solid rgba(254, 249, 225, .32);
432
- padding: 6px 8px;
433
- font-family: "SFMono-Regular", Consolas, monospace;
434
- font-size: 12px;
381
+ flex-direction: column;
435
382
  }
436
383
 
437
- .mop-assistant-conversation { flex: 1; overflow-y: auto; padding: 0 28px; }
384
+ .mop-assistant-conversation { flex: 1; overflow-y: auto; padding: 0 clamp(18px, 5vw, 64px); }
438
385
  .mop-assistant-welcome {
439
- min-height: calc(100vh - 285px);
386
+ width: min(100%, 760px);
387
+ min-height: calc(100vh - 190px);
388
+ margin: 0 auto;
440
389
  display: flex;
441
390
  flex-direction: column;
442
391
  align-items: center;
443
392
  justify-content: center;
444
- padding: 32px 0 90px;
393
+ padding: 38px 0 110px;
445
394
  text-align: center;
446
395
  }
447
396
 
@@ -454,6 +403,104 @@ button {
454
403
  background: linear-gradient(transparent, var(--mop-cream) 28%);
455
404
  }
456
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
+
457
504
  .mop-settings-grid {
458
505
  display: grid;
459
506
  grid-template-columns: 1fr;
@@ -497,11 +544,10 @@ button {
497
544
  .mop-app-topbar { grid-template-columns: 66px minmax(0, 1fr); }
498
545
  .mop-app-brand { justify-content: center; padding: 5px; }
499
546
  .mop-app-brand img { width: 49px; height: 49px; }
500
- .mop-app-brand span, .mop-topbar-center { display: none; }
547
+ .mop-app-brand span { display: none; }
501
548
  .mop-app-topbar-main { padding: 0 10px; gap: 9px; }
502
549
  .mop-menu-toggle { display: block; }
503
- .mop-topbar-title { min-width: 0; font-size: 11px; }
504
- .mop-topbar-meta > span:first-child { display: none; }
550
+ .mop-topbar-center { min-width: 0; flex: 1; padding: 8px 12px; font-size: 10px; }
505
551
  .mop-app-sidebar {
506
552
  position: fixed;
507
553
  top: 62px;
@@ -522,9 +568,8 @@ button {
522
568
  background: rgba(20, 34, 29, .56);
523
569
  }
524
570
  .mop-app-main { min-height: calc(100vh - 62px); }
525
- .mop-assistant-page { min-height: calc(100vh - 62px); }
526
- .mop-assistant-toolbar { gap: 9px; }
527
- .mop-assistant-status { display: none; }
571
+ .mop-assistant-page { height: calc(100vh - 62px); min-height: 520px; grid-template-columns: minmax(0, 1fr); }
572
+ .mop-chat-history { display: none; }
528
573
  .mop-assistant-conversation { padding: 0 16px; }
529
574
  .mop-assistant-composer-wrap { padding: 26px 12px 12px; }
530
575
  .mop-settings-grid { grid-template-columns: 1fr; }
@@ -535,7 +580,6 @@ button {
535
580
  display: flex;
536
581
  align-items: center;
537
582
  justify-content: center;
538
- gap: 8px;
539
583
  width: 100%;
540
584
  min-height: 40px;
541
585
  margin-bottom: 9px;
@@ -547,19 +591,12 @@ button {
547
591
  font-size: 11px;
548
592
  font-weight: 900;
549
593
  text-decoration: none;
550
- cursor: pointer;
551
- transition: transform 80ms steps(2, end), box-shadow 80ms steps(2, end);
552
- box-shadow: 2px 2px 0 rgba(45, 74, 62, .17);
594
+ box-shadow: 2px 2px 0 rgba(18, 38, 30, .28);
553
595
  }
554
596
 
555
- .mop-back-workspace-btn:hover {
556
- transform: translate(-1px, -1px);
557
- box-shadow: 3px 3px 0 rgba(45, 74, 62, .24);
558
- color: var(--mop-cream);
559
- }
597
+ .mop-back-workspace-btn:hover { color: var(--mop-cream); transform: translate(-1px, -1px); }
560
598
 
561
- .mop-back-workspace-btn:active {
562
- transform: translate(1px, 1px);
563
- box-shadow: 0 0 0 rgba(45, 74, 62, 0);
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; }
564
602
  }
565
-
@@ -12,13 +12,9 @@ export type AppViewer = {
12
12
  };
13
13
 
14
14
  export type Project = { id: string; name: string; status: string };
15
- export type ProviderState = { configured: boolean; provider?: string; model?: string | null };
16
15
 
17
16
  interface MemoryCoreContextType {
18
- selectedProject: string;
19
- setSelectedProject: (id: string) => void;
20
17
  projects: Project[];
21
- provider: ProviderState;
22
18
  settingsSection: "providers" | "users";
23
19
  setSettingsSection: (section: "providers" | "users") => void;
24
20
  }
@@ -27,9 +23,7 @@ const MemoryCoreContext = createContext<MemoryCoreContextType | undefined>(undef
27
23
 
28
24
  export function useMemoryCore() {
29
25
  const context = useContext(MemoryCoreContext);
30
- if (!context) {
31
- throw new Error("useMemoryCore must be used within a MemoryCoreProvider");
32
- }
26
+ if (!context) throw new Error("useMemoryCore must be used within a MemoryCoreProvider");
33
27
  return context;
34
28
  }
35
29
 
@@ -46,22 +40,17 @@ function pageTitle(pathname: string): string {
46
40
  export function AppShell({ viewer, children }: { viewer: AppViewer; children: ReactNode }) {
47
41
  const pathname = usePathname();
48
42
  const [menuOpen, setMenuOpen] = useState(false);
49
- const isAdmin = viewer.role === "owner";
50
- const title = pageTitle(pathname);
51
-
52
43
  const [projects, setProjects] = useState<Project[]>([]);
53
- const [provider, setProvider] = useState<ProviderState>({ configured: false });
54
- const [selectedProject, setSelectedProject] = useState("");
55
44
  const [settingsSection, setSettingsSection] = useState<"providers" | "users">("providers");
45
+ const isAdmin = viewer.role === "owner";
46
+ const isSettings = pathname.startsWith("/settings");
47
+ const title = pageTitle(pathname);
56
48
 
57
49
  useEffect(() => {
58
- Promise.all([
59
- fetch("/api/projects").then((r) => r.json()),
60
- fetch("/api/providers").then((r) => r.json()),
61
- ]).then(([projectData, providerData]) => {
62
- setProjects(projectData.projects ?? []);
63
- setProvider(providerData.config ?? { configured: false });
64
- }).catch(() => {});
50
+ fetch("/api/projects")
51
+ .then((response) => response.json())
52
+ .then((data) => setProjects(data.projects ?? []))
53
+ .catch(() => {});
65
54
 
66
55
  const requested = new URLSearchParams(window.location.search).get("section");
67
56
  if (requested === "users") setSettingsSection("users");
@@ -72,21 +61,13 @@ export function AppShell({ viewer, children }: { viewer: AppViewer; children: Re
72
61
  window.location.replace("/login");
73
62
  }
74
63
 
75
- const selectSection = (sec: "providers" | "users") => {
76
- setSettingsSection(sec);
77
- const url = sec === "providers" ? "/settings" : "/settings?section=users";
78
- window.history.replaceState(null, "", url);
79
- };
80
-
81
- const isSettings = pathname.startsWith("/settings");
82
-
83
- const nav = [
84
- { href: "/assistant", label: "Assistant", icon: "✦", active: pathname.startsWith("/assistant") || pathname.startsWith("/chat/") },
85
- { href: "/brain", label: "Brain", icon: "◉", active: pathname.startsWith("/brain") },
86
- ];
64
+ function selectSection(section: "providers" | "users") {
65
+ setSettingsSection(section);
66
+ window.history.replaceState(null, "", section === "providers" ? "/settings" : "/settings?section=users");
67
+ }
87
68
 
88
69
  return (
89
- <MemoryCoreContext.Provider value={{ selectedProject, setSelectedProject, projects, provider, settingsSection, setSettingsSection }}>
70
+ <MemoryCoreContext.Provider value={{ projects, settingsSection, setSettingsSection }}>
90
71
  <div className="mop-app-frame">
91
72
  <header className="mop-app-topbar">
92
73
  <a className="mop-app-brand" href="/assistant" aria-label="MOP-AGENT home">
@@ -103,36 +84,10 @@ export function AppShell({ viewer, children }: { viewer: AppViewer; children: Re
103
84
  >
104
85
 
105
86
  </button>
106
- {pathname === "/assistant" ? (
107
- <div className="mop-assistant-toolbar">
108
- <div className="mop-assistant-status">
109
- <span className="mop-live-dot" />
110
- <strong>LIVE ASSISTANT</strong>
111
- <span className="mop-assistant-provider">
112
- {provider.configured ? `${provider.provider}${provider.model ? ` · ${provider.model}` : ""}` : "offline demo"}
113
- </span>
114
- </div>
115
- <label className="mop-assistant-scope">
116
- MEMORY SCOPE
117
- <select value={selectedProject} onChange={(e) => setSelectedProject(e.target.value)}>
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
- </div>
123
- ) : (
124
- <>
125
- <div className="mop-topbar-title">
126
- <span className="mop-live-dot" />
127
- <strong>{title}</strong>
128
- </div>
129
- <div className="mop-topbar-center">MOP MEMORYCORE</div>
130
- <div className="mop-topbar-meta">
131
- <span>{isAdmin ? "ADMIN" : "MEMBER"}</span>
132
- <span className="mop-version">v0.1.12</span>
133
- </div>
134
- </>
135
- )}
87
+ <div className="mop-topbar-center">
88
+ <span className="mop-live-dot" />
89
+ <strong>{title}</strong>
90
+ </div>
136
91
  </div>
137
92
  </header>
138
93
 
@@ -155,23 +110,22 @@ export function AppShell({ viewer, children }: { viewer: AppViewer; children: Re
155
110
  </div>
156
111
  ) : (
157
112
  <>
158
- <div className="mop-nav-section">
159
- <p>WORKSPACE</p>
160
- <nav>
161
- {nav.map((item) => (
162
- <a key={item.href} href={item.href} className={item.active ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
163
- <span className="mop-nav-icon">{item.icon}</span>
164
- <span>{item.label}</span>
165
- </a>
166
- ))}
167
- </nav>
168
- </div>
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>
169
123
 
170
124
  {isAdmin && (
171
- <div className="mop-nav-section">
125
+ <div className="mop-nav-section mop-admin-nav">
172
126
  <p>ADMIN</p>
173
127
  <nav>
174
- <a href="/settings" className={pathname.startsWith("/settings") ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
128
+ <a href="/settings" onClick={() => setMenuOpen(false)}>
175
129
  <span className="mop-nav-icon">⚙</span>
176
130
  <span>Settings</span>
177
131
  </a>
@@ -182,20 +136,18 @@ export function AppShell({ viewer, children }: { viewer: AppViewer; children: Re
182
136
  )}
183
137
 
184
138
  <div className="mop-sidebar-spacer" />
185
-
186
139
  {isSettings && (
187
- <a href="/assistant" className="mop-back-workspace-btn">
140
+ <a href="/assistant" className="mop-back-workspace-btn" onClick={() => setMenuOpen(false)}>
188
141
  <span>← BACK TO WORKSPACE</span>
189
142
  </a>
190
143
  )}
191
-
192
144
  <button className="mop-account-card" type="button" onClick={logout} title="Sign out">
193
145
  <span className="mop-account-avatar">{viewer.name.slice(0, 1).toUpperCase()}</span>
194
146
  <span className="mop-account-copy">
195
147
  <strong>{viewer.name}</strong>
196
148
  <small>{isAdmin ? "Administrator" : "Member"}</small>
197
149
  </span>
198
- <span aria-hidden="true">↪</span>
150
+ <span aria-hidden="true">•••</span>
199
151
  </button>
200
152
  </aside>
201
153
 
@@ -204,4 +156,3 @@ export function AppShell({ viewer, children }: { viewer: AppViewer; children: Re
204
156
  </MemoryCoreContext.Provider>
205
157
  );
206
158
  }
207
-
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "mop-agent",
3
- "version": "0.1.12",
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.12",
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.12",
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",