@hienlh/ppm 0.9.42 → 0.9.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/web/assets/{chat-tab-BSQOFDle.js → chat-tab-DvNEQYEe.js} +1 -1
- package/dist/web/assets/{code-editor-eDYb_XML.js → code-editor-CoT017Ah.js} +1 -1
- package/dist/web/assets/{database-viewer-nP78XqEF.js → database-viewer-C3wK7cDk.js} +1 -1
- package/dist/web/assets/{diff-viewer-DTMtBxHM.js → diff-viewer-D0tuen4I.js} +1 -1
- package/dist/web/assets/{extension-webview-DzWz--CI.js → extension-webview-Ba5aeo9r.js} +1 -1
- package/dist/web/assets/{git-graph-D_6NTVVT.js → git-graph-BnJrVPxJ.js} +1 -1
- package/dist/web/assets/index-DUQgLj0D.js +30 -0
- package/dist/web/assets/keybindings-store-CkGFjxkX.js +1 -0
- package/dist/web/assets/{markdown-renderer-CxJg37If.js → markdown-renderer-BuGSrE3y.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-DBBJ3z8x.js → port-forwarding-tab-DsbrWNUP.js} +1 -1
- package/dist/web/assets/{postgres-viewer-CQ3coJ1p.js → postgres-viewer-Bh6YmZPq.js} +1 -1
- package/dist/web/assets/{settings-tab-CE8H5NiY.js → settings-tab-BnzFtexC.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-Ccm-un47.js → sqlite-viewer-Cu3_hf07.js} +1 -1
- package/dist/web/assets/{terminal-tab-DnlFNbY6.js → terminal-tab-fnZvscaH.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-hwg4tMW2.js → use-monaco-theme-BdcKAZ69.js} +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/sw.js +1 -1
- package/docs/streaming-input-guide.md +267 -0
- package/package.json +1 -1
- package/snapshot-state.md +1526 -0
- package/src/server/routes/settings.ts +20 -1
- package/src/services/ppmbot/ppmbot-service.ts +96 -12
- package/src/services/ppmbot/ppmbot-telegram.ts +3 -2
- package/src/web/components/settings/ppmbot-settings-section.tsx +84 -2
- package/test-session-ops.mjs +444 -0
- package/test-tokens.mjs +212 -0
- package/dist/web/assets/index-D48IQVYU.js +0 -30
- package/dist/web/assets/keybindings-store-BaWyhjXJ.js +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { configService } from "../../services/config.service.ts";
|
|
3
|
-
import { getConfigValue, setConfigValue, listPairedChats, getPairingByCode, approvePairing, revokePairing } from "../../services/db.service.ts";
|
|
3
|
+
import { getConfigValue, setConfigValue, listPairedChats, getPairingByCode, approvePairing, revokePairing, getPPMBotMemories, getDb } from "../../services/db.service.ts";
|
|
4
4
|
import {
|
|
5
5
|
validateAIProviderConfig,
|
|
6
6
|
validateDefaultProvider,
|
|
@@ -382,3 +382,22 @@ settingsRoutes.delete("/clawbot/paired/:chatId", (c) => {
|
|
|
382
382
|
revokePairing(c.req.param("chatId"));
|
|
383
383
|
return c.json(ok({ revoked: true }));
|
|
384
384
|
});
|
|
385
|
+
|
|
386
|
+
/** GET /settings/clawbot/memories?project=xxx — list memories for a project */
|
|
387
|
+
settingsRoutes.get("/clawbot/memories", (c) => {
|
|
388
|
+
const project = c.req.query("project") || "_global";
|
|
389
|
+
const memories = getPPMBotMemories(project, 50);
|
|
390
|
+
return c.json(ok(memories));
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
/** DELETE /settings/clawbot/memories/:id — delete a specific memory */
|
|
394
|
+
settingsRoutes.delete("/clawbot/memories/:id", (c) => {
|
|
395
|
+
const id = Number(c.req.param("id"));
|
|
396
|
+
if (!id) return c.json(err("Invalid memory ID"), 400);
|
|
397
|
+
try {
|
|
398
|
+
getDb().query("DELETE FROM clawbot_memories WHERE id = ?").run(id);
|
|
399
|
+
return c.json(ok({ deleted: id }));
|
|
400
|
+
} catch (e) {
|
|
401
|
+
return c.json(err((e as Error).message), 500);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
getPairingByChatId,
|
|
6
6
|
createPairingRequest,
|
|
7
7
|
approvePairing,
|
|
8
|
+
getSessionTitles,
|
|
9
|
+
getApprovedPairedChats,
|
|
8
10
|
} from "../db.service.ts";
|
|
9
11
|
import { PPMBotTelegram } from "./ppmbot-telegram.ts";
|
|
10
12
|
import { PPMBotSessionManager } from "./ppmbot-session.ts";
|
|
@@ -39,6 +41,9 @@ class PPMBotService {
|
|
|
39
41
|
/** Chat IDs that just received identity onboarding prompt */
|
|
40
42
|
private identityPending = new Set<string>();
|
|
41
43
|
|
|
44
|
+
/** Chat IDs where we've already checked for identity (once per session) */
|
|
45
|
+
private hasCheckedIdentity = new Set<string>();
|
|
46
|
+
|
|
42
47
|
/** Message count per session for periodic memory save */
|
|
43
48
|
private messageCount = new Map<string, number>();
|
|
44
49
|
|
|
@@ -70,6 +75,9 @@ class PPMBotService {
|
|
|
70
75
|
// Start polling (non-blocking)
|
|
71
76
|
this.telegram.startPolling((update) => this.handleUpdate(update));
|
|
72
77
|
|
|
78
|
+
// Check if this is a restart and notify users
|
|
79
|
+
await this.checkRestartNotification();
|
|
80
|
+
|
|
73
81
|
console.log("[ppmbot] Started");
|
|
74
82
|
} catch (err) {
|
|
75
83
|
console.error("[ppmbot] Start failed:", (err as Error).message);
|
|
@@ -87,6 +95,7 @@ class PPMBotService {
|
|
|
87
95
|
this.processing.clear();
|
|
88
96
|
this.messageQueue.clear();
|
|
89
97
|
this.identityPending.clear();
|
|
98
|
+
this.hasCheckedIdentity.clear();
|
|
90
99
|
this.messageCount.clear();
|
|
91
100
|
|
|
92
101
|
console.log("[ppmbot] Stopped");
|
|
@@ -172,6 +181,7 @@ class PPMBotService {
|
|
|
172
181
|
case "memory": await this.cmdMemory(chatId); break;
|
|
173
182
|
case "forget": await this.cmdForget(chatId, cmd.args); break;
|
|
174
183
|
case "remember": await this.cmdRemember(chatId, cmd.args); break;
|
|
184
|
+
case "restart": await this.cmdRestart(chatId); break;
|
|
175
185
|
case "help": await this.cmdHelp(chatId); break;
|
|
176
186
|
default: await tg.sendMessage(Number(chatId), `Unknown command: /${cmd.command}`);
|
|
177
187
|
}
|
|
@@ -201,8 +211,11 @@ class PPMBotService {
|
|
|
201
211
|
await this.telegram!.sendMessage(Number(chatId), text);
|
|
202
212
|
|
|
203
213
|
// Identity onboarding: if no identity memories exist, ask user
|
|
204
|
-
const
|
|
205
|
-
|
|
214
|
+
const globalMemories = this.memory.getSummary("_global", 50);
|
|
215
|
+
const hasIdentity = globalMemories.some((m) =>
|
|
216
|
+
m.category === "preference" && /identity|name|role/i.test(m.content),
|
|
217
|
+
);
|
|
218
|
+
if (!hasIdentity) {
|
|
206
219
|
this.identityPending.add(chatId);
|
|
207
220
|
await this.telegram!.sendMessage(
|
|
208
221
|
Number(chatId),
|
|
@@ -220,11 +233,20 @@ class PPMBotService {
|
|
|
220
233
|
private async cmdProject(chatId: string, args: string): Promise<void> {
|
|
221
234
|
if (!args) {
|
|
222
235
|
const active = this.sessions.getActiveSession(chatId);
|
|
223
|
-
const current = active?.projectName ?? "
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
236
|
+
const current = active?.projectName ?? "";
|
|
237
|
+
const projects = this.sessions.getProjectNames();
|
|
238
|
+
let text = "<b>Projects</b>\n\n";
|
|
239
|
+
if (projects.length === 0) {
|
|
240
|
+
text += "No projects configured.\nUsing default: <code>~/.ppm/bot/</code>\n";
|
|
241
|
+
} else {
|
|
242
|
+
for (const name of projects) {
|
|
243
|
+
const marker = name === current ? " ✓" : "";
|
|
244
|
+
text += `• <code>${escapeHtml(name)}</code>${marker}\n`;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
text += `\nCurrent: <b>${escapeHtml(current || "bot (default)")}</b>`;
|
|
248
|
+
text += "\nSwitch: /project <name>";
|
|
249
|
+
await this.telegram!.sendMessage(Number(chatId), text);
|
|
228
250
|
return;
|
|
229
251
|
}
|
|
230
252
|
|
|
@@ -254,13 +276,23 @@ class PPMBotService {
|
|
|
254
276
|
await this.telegram!.sendMessage(Number(chatId), "No recent sessions.");
|
|
255
277
|
return;
|
|
256
278
|
}
|
|
279
|
+
|
|
280
|
+
// Fetch titles for all sessions
|
|
281
|
+
const titles = getSessionTitles(sessions.map((s) => s.session_id));
|
|
282
|
+
|
|
257
283
|
let text = "<b>Recent Sessions</b>\n\n";
|
|
258
284
|
sessions.forEach((s, i) => {
|
|
259
285
|
const active = s.is_active ? " ⬤" : "";
|
|
260
|
-
const
|
|
261
|
-
|
|
286
|
+
const title = titles[s.session_id]?.replace(/^\[PPM\]\s*/, "") || "";
|
|
287
|
+
const preview = title ? ` — ${escapeHtml(title.slice(0, 40))}` : "";
|
|
288
|
+
const date = new Date(s.last_message_at * 1000).toLocaleString(undefined, {
|
|
289
|
+
month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
|
|
290
|
+
});
|
|
291
|
+
const sid = s.session_id.slice(0, 8);
|
|
292
|
+
text += `${i + 1}. <b>${escapeHtml(s.project_name)}</b>${preview}${active}\n`;
|
|
293
|
+
text += ` <code>${sid}</code> · ${date}\n\n`;
|
|
262
294
|
});
|
|
263
|
-
text += "
|
|
295
|
+
text += "Resume: /resume <number>";
|
|
264
296
|
await this.telegram!.sendMessage(Number(chatId), text);
|
|
265
297
|
}
|
|
266
298
|
|
|
@@ -338,11 +370,54 @@ class PPMBotService {
|
|
|
338
370
|
await this.telegram!.sendMessage(Number(chatId), "Remembered ✓");
|
|
339
371
|
}
|
|
340
372
|
|
|
373
|
+
private async cmdRestart(chatId: string): Promise<void> {
|
|
374
|
+
await this.telegram!.sendMessage(Number(chatId), "🔄 Restarting PPM...");
|
|
375
|
+
|
|
376
|
+
// Schedule restart after a short delay so the response is sent
|
|
377
|
+
setTimeout(async () => {
|
|
378
|
+
const { join } = await import("node:path");
|
|
379
|
+
const { writeFileSync } = await import("node:fs");
|
|
380
|
+
const { homedir } = await import("node:os");
|
|
381
|
+
|
|
382
|
+
const approvedChats = getApprovedPairedChats();
|
|
383
|
+
const chatIds = approvedChats.map((c) => c.telegram_chat_id);
|
|
384
|
+
|
|
385
|
+
// Write restart marker so we can notify after restart
|
|
386
|
+
const markerPath = join(homedir(), ".ppm", "restart-notify.json");
|
|
387
|
+
writeFileSync(markerPath, JSON.stringify({ chatIds, ts: Date.now() }));
|
|
388
|
+
|
|
389
|
+
console.log("[ppmbot] Restart requested via Telegram, exiting...");
|
|
390
|
+
process.exit(0);
|
|
391
|
+
}, 500);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Check for restart notification marker and send notification */
|
|
395
|
+
async checkRestartNotification(): Promise<void> {
|
|
396
|
+
try {
|
|
397
|
+
const { join } = await import("node:path");
|
|
398
|
+
const { existsSync, readFileSync, unlinkSync } = await import("node:fs");
|
|
399
|
+
const { homedir } = await import("node:os");
|
|
400
|
+
|
|
401
|
+
const markerPath = join(homedir(), ".ppm", "restart-notify.json");
|
|
402
|
+
if (!existsSync(markerPath)) return;
|
|
403
|
+
|
|
404
|
+
const data = JSON.parse(readFileSync(markerPath, "utf-8"));
|
|
405
|
+
unlinkSync(markerPath);
|
|
406
|
+
|
|
407
|
+
// Only notify if restart was recent (< 60s)
|
|
408
|
+
if (Date.now() - data.ts > 60_000) return;
|
|
409
|
+
|
|
410
|
+
for (const cid of data.chatIds) {
|
|
411
|
+
await this.telegram?.sendMessage(Number(cid), "✅ PPM restarted successfully.");
|
|
412
|
+
}
|
|
413
|
+
} catch {}
|
|
414
|
+
}
|
|
415
|
+
|
|
341
416
|
private async cmdHelp(chatId: string): Promise<void> {
|
|
342
417
|
const text = `<b>PPMBot Commands</b>
|
|
343
418
|
|
|
344
419
|
/start — Greeting + list projects
|
|
345
|
-
/project <name> — Switch
|
|
420
|
+
/project <name> — Switch/list projects
|
|
346
421
|
/new — Fresh session (current project)
|
|
347
422
|
/sessions — List recent sessions
|
|
348
423
|
/resume <n> — Resume session #n
|
|
@@ -351,6 +426,7 @@ class PPMBotService {
|
|
|
351
426
|
/memory — Show project memories
|
|
352
427
|
/forget <topic> — Remove matching memories
|
|
353
428
|
/remember <fact> — Save a fact
|
|
429
|
+
/restart — Restart PPM server
|
|
354
430
|
/help — This message`;
|
|
355
431
|
await this.telegram!.sendMessage(Number(chatId), text);
|
|
356
432
|
}
|
|
@@ -442,11 +518,19 @@ class PPMBotService {
|
|
|
442
518
|
},
|
|
443
519
|
);
|
|
444
520
|
|
|
445
|
-
// Capture identity
|
|
521
|
+
// Capture identity: save when onboarding was shown OR when no identity exists
|
|
522
|
+
// (handles server restarts losing the in-memory flag)
|
|
446
523
|
if (this.identityPending.has(chatId)) {
|
|
447
524
|
this.identityPending.delete(chatId);
|
|
448
525
|
this.memory.saveOne("_global", `User identity: ${text}`, "preference", session.sessionId);
|
|
449
526
|
console.log("[ppmbot] Saved identity memory from onboarding");
|
|
527
|
+
} else if (!this.hasCheckedIdentity.has(chatId)) {
|
|
528
|
+
this.hasCheckedIdentity.add(chatId);
|
|
529
|
+
const globalMems = this.memory.getSummary("_global", 50);
|
|
530
|
+
if (!globalMems.some((m) => m.category === "preference" && /identity/i.test(m.content))) {
|
|
531
|
+
this.memory.saveOne("_global", `User identity: ${text}`, "preference", session.sessionId);
|
|
532
|
+
console.log("[ppmbot] Saved identity memory (first message, no identity found)");
|
|
533
|
+
}
|
|
450
534
|
}
|
|
451
535
|
|
|
452
536
|
// Periodic memory extraction — fire-and-forget every N messages
|
|
@@ -13,7 +13,7 @@ const BOT_TOKEN_RE = /^\d+:[A-Za-z0-9_-]{30,50}$/;
|
|
|
13
13
|
/** Known PPMBot slash commands */
|
|
14
14
|
const COMMANDS = new Set([
|
|
15
15
|
"start", "project", "new", "sessions", "resume",
|
|
16
|
-
"status", "stop", "memory", "forget", "remember", "help",
|
|
16
|
+
"status", "stop", "memory", "forget", "remember", "restart", "help",
|
|
17
17
|
]);
|
|
18
18
|
|
|
19
19
|
export type UpdateHandler = (update: TelegramUpdate) => Promise<void>;
|
|
@@ -43,7 +43,7 @@ export class PPMBotTelegram {
|
|
|
43
43
|
await this.callApi("setMyCommands", {
|
|
44
44
|
commands: [
|
|
45
45
|
{ command: "start", description: "Greeting + list projects" },
|
|
46
|
-
{ command: "project", description: "Switch
|
|
46
|
+
{ command: "project", description: "Switch/list projects" },
|
|
47
47
|
{ command: "new", description: "Fresh session (current project)" },
|
|
48
48
|
{ command: "sessions", description: "List recent sessions" },
|
|
49
49
|
{ command: "resume", description: "Resume a previous session" },
|
|
@@ -52,6 +52,7 @@ export class PPMBotTelegram {
|
|
|
52
52
|
{ command: "memory", description: "Show project memories" },
|
|
53
53
|
{ command: "forget", description: "Remove matching memories" },
|
|
54
54
|
{ command: "remember", description: "Save a fact" },
|
|
55
|
+
{ command: "restart", description: "Restart PPM server" },
|
|
55
56
|
{ command: "help", description: "Show all commands" },
|
|
56
57
|
],
|
|
57
58
|
});
|
|
@@ -3,7 +3,8 @@ import { Button } from "@/components/ui/button";
|
|
|
3
3
|
import { Input } from "@/components/ui/input";
|
|
4
4
|
import { Switch } from "@/components/ui/switch";
|
|
5
5
|
import { api } from "@/lib/api-client";
|
|
6
|
-
import { Trash2, CheckCircle, Clock, Send } from "lucide-react";
|
|
6
|
+
import { Trash2, CheckCircle, Clock, Send, Brain, RefreshCw } from "lucide-react";
|
|
7
|
+
import { Separator } from "@/components/ui/separator";
|
|
7
8
|
|
|
8
9
|
interface PPMBotConfig {
|
|
9
10
|
enabled: boolean;
|
|
@@ -20,6 +21,14 @@ interface TelegramConfig {
|
|
|
20
21
|
bot_token: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
interface MemoryRow {
|
|
25
|
+
id: number;
|
|
26
|
+
project: string;
|
|
27
|
+
content: string;
|
|
28
|
+
category: string;
|
|
29
|
+
importance: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
interface PairedChat {
|
|
24
33
|
id: number;
|
|
25
34
|
telegram_chat_id: string;
|
|
@@ -53,6 +62,9 @@ export function PPMBotSettingsSection() {
|
|
|
53
62
|
const [approving, setApproving] = useState(false);
|
|
54
63
|
const [testing, setTesting] = useState(false);
|
|
55
64
|
|
|
65
|
+
const [memories, setMemories] = useState<MemoryRow[]>([]);
|
|
66
|
+
const [memoryProject, setMemoryProject] = useState("_global");
|
|
67
|
+
|
|
56
68
|
const fetchPairedChats = useCallback(async () => {
|
|
57
69
|
try {
|
|
58
70
|
const data = await api.get<PairedChat[]>("/api/settings/clawbot/paired");
|
|
@@ -60,6 +72,20 @@ export function PPMBotSettingsSection() {
|
|
|
60
72
|
} catch {}
|
|
61
73
|
}, []);
|
|
62
74
|
|
|
75
|
+
const fetchMemories = useCallback(async (project = memoryProject) => {
|
|
76
|
+
try {
|
|
77
|
+
const data = await api.get<MemoryRow[]>(`/api/settings/clawbot/memories?project=${encodeURIComponent(project)}`);
|
|
78
|
+
setMemories(data);
|
|
79
|
+
} catch {}
|
|
80
|
+
}, [memoryProject]);
|
|
81
|
+
|
|
82
|
+
const deleteMemory = async (id: number) => {
|
|
83
|
+
try {
|
|
84
|
+
await api.del(`/api/settings/clawbot/memories/${id}`);
|
|
85
|
+
setMemories((prev) => prev.filter((m) => m.id !== id));
|
|
86
|
+
} catch {}
|
|
87
|
+
};
|
|
88
|
+
|
|
63
89
|
useEffect(() => {
|
|
64
90
|
api.get<PPMBotConfig>("/api/settings/clawbot").then((data) => {
|
|
65
91
|
setConfig(data);
|
|
@@ -74,7 +100,8 @@ export function PPMBotSettingsSection() {
|
|
|
74
100
|
setTokenConfigured(!!data.bot_token);
|
|
75
101
|
}).catch(() => {});
|
|
76
102
|
fetchPairedChats();
|
|
77
|
-
|
|
103
|
+
fetchMemories("_global");
|
|
104
|
+
}, [fetchPairedChats, fetchMemories]);
|
|
78
105
|
|
|
79
106
|
const saveToken = async () => {
|
|
80
107
|
if (!tokenInput.trim()) return;
|
|
@@ -276,6 +303,61 @@ export function PPMBotSettingsSection() {
|
|
|
276
303
|
)}
|
|
277
304
|
</div>
|
|
278
305
|
|
|
306
|
+
<Separator />
|
|
307
|
+
|
|
308
|
+
{/* Memory & Identity */}
|
|
309
|
+
<div className="space-y-2">
|
|
310
|
+
<div className="flex items-center justify-between">
|
|
311
|
+
<div className="flex items-center gap-1.5">
|
|
312
|
+
<Brain className="size-3.5 text-muted-foreground" />
|
|
313
|
+
<p className="text-xs font-medium">Memory & Identity</p>
|
|
314
|
+
</div>
|
|
315
|
+
<Button
|
|
316
|
+
variant="ghost"
|
|
317
|
+
size="sm"
|
|
318
|
+
className="h-6 w-6 p-0 cursor-pointer"
|
|
319
|
+
onClick={() => fetchMemories(memoryProject)}
|
|
320
|
+
>
|
|
321
|
+
<RefreshCw className="size-3" />
|
|
322
|
+
</Button>
|
|
323
|
+
</div>
|
|
324
|
+
<p className="text-[10px] text-muted-foreground">
|
|
325
|
+
Facts the bot remembers across sessions. Use /remember on Telegram to add, or delete here.
|
|
326
|
+
</p>
|
|
327
|
+
|
|
328
|
+
{memories.length === 0 ? (
|
|
329
|
+
<p className="text-[10px] text-muted-foreground italic">
|
|
330
|
+
No memories stored yet. Send /start on Telegram and introduce yourself.
|
|
331
|
+
</p>
|
|
332
|
+
) : (
|
|
333
|
+
<div className="space-y-1 max-h-[200px] overflow-y-auto">
|
|
334
|
+
{memories.map((mem) => (
|
|
335
|
+
<div
|
|
336
|
+
key={mem.id}
|
|
337
|
+
className="flex items-start justify-between rounded-md border p-2 gap-1"
|
|
338
|
+
>
|
|
339
|
+
<div className="min-w-0 flex-1">
|
|
340
|
+
<p className="text-[11px] break-words">{mem.content}</p>
|
|
341
|
+
<p className="text-[10px] text-muted-foreground">
|
|
342
|
+
{mem.category} · {mem.project}
|
|
343
|
+
</p>
|
|
344
|
+
</div>
|
|
345
|
+
<Button
|
|
346
|
+
variant="ghost"
|
|
347
|
+
size="sm"
|
|
348
|
+
className="h-6 w-6 p-0 text-destructive hover:text-destructive cursor-pointer shrink-0"
|
|
349
|
+
onClick={() => deleteMemory(mem.id)}
|
|
350
|
+
>
|
|
351
|
+
<Trash2 className="size-3" />
|
|
352
|
+
</Button>
|
|
353
|
+
</div>
|
|
354
|
+
))}
|
|
355
|
+
</div>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
<Separator />
|
|
360
|
+
|
|
279
361
|
{/* Default Project */}
|
|
280
362
|
<div className="space-y-1.5">
|
|
281
363
|
<label className="text-[11px] text-muted-foreground">Default Project</label>
|