@hienlh/ppm 0.9.49 โ†’ 0.9.50

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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.50] - 2026-04-07
4
+
5
+ ### Changed
6
+ - **/sessions redesigned**: Filters by current project, shows pinned sessions first with ๐Ÿ“Œ icon, displays session titles (not project names), supports pagination via `/sessions 2`. Matches PPM web UI behavior.
7
+
3
8
  ## [0.9.49] - 2026-04-07
4
9
 
5
10
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.9.49",
3
+ "version": "0.9.50",
4
4
  "description": "Personal Project Manager โ€” mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -5,6 +5,7 @@ import {
5
5
  getPairingByChatId,
6
6
  createPairingRequest,
7
7
  getSessionTitles,
8
+ getPinnedSessionIds,
8
9
  getApprovedPairedChats,
9
10
  } from "../db.service.ts";
10
11
  import { PPMBotTelegram } from "./ppmbot-telegram.ts";
@@ -159,7 +160,7 @@ class PPMBotService {
159
160
  case "start": await this.cmdStart(chatId); break;
160
161
  case "project": await this.cmdProject(chatId, cmd.args); break;
161
162
  case "new": await this.cmdNew(chatId); break;
162
- case "sessions": await this.cmdSessions(chatId); break;
163
+ case "sessions": await this.cmdSessions(chatId, cmd.args); break;
163
164
  case "resume": await this.cmdResume(chatId, cmd.args); break;
164
165
  case "status": await this.cmdStatus(chatId); break;
165
166
  case "stop": await this.cmdStop(chatId); break;
@@ -254,29 +255,73 @@ class PPMBotService {
254
255
  );
255
256
  }
256
257
 
257
- private async cmdSessions(chatId: string): Promise<void> {
258
- const sessions = this.sessions.listRecentSessions(chatId, 10);
259
- if (sessions.length === 0) {
260
- await this.telegram!.sendMessage(Number(chatId), "No recent sessions.");
258
+ private async cmdSessions(chatId: string, args: string): Promise<void> {
259
+ const PAGE_SIZE = 8;
260
+ const page = Math.max(1, parseInt(args, 10) || 1);
261
+
262
+ const active = this.sessions.getActiveSession(chatId);
263
+ const project = active?.projectName;
264
+
265
+ // Fetch all sessions for this chat (enough for pagination)
266
+ const allSessions = this.sessions.listRecentSessions(chatId, 50);
267
+ // Filter by current project if one is active
268
+ const filtered = project
269
+ ? allSessions.filter((s) => s.project_name === project)
270
+ : allSessions;
271
+
272
+ if (filtered.length === 0) {
273
+ await this.telegram!.sendMessage(Number(chatId), "No sessions yet. Send a message to start.");
261
274
  return;
262
275
  }
263
276
 
264
- // Fetch titles for all sessions
265
- const titles = getSessionTitles(sessions.map((s) => s.session_id));
277
+ // Enrich with titles and pin status
278
+ const titles = getSessionTitles(filtered.map((s) => s.session_id));
279
+ const pinnedIds = getPinnedSessionIds();
280
+
281
+ // Sort: pinned first, then by last_message_at desc
282
+ const sorted = [...filtered].sort((a, b) => {
283
+ const aPin = pinnedIds.has(a.session_id) ? 1 : 0;
284
+ const bPin = pinnedIds.has(b.session_id) ? 1 : 0;
285
+ if (aPin !== bPin) return bPin - aPin;
286
+ return b.last_message_at - a.last_message_at;
287
+ });
288
+
289
+ const totalPages = Math.ceil(sorted.length / PAGE_SIZE);
290
+ const start = (page - 1) * PAGE_SIZE;
291
+ const pageItems = sorted.slice(start, start + PAGE_SIZE);
266
292
 
267
- let text = "<b>Recent Sessions</b>\n\n";
268
- sessions.forEach((s, i) => {
269
- const active = s.is_active ? " โฌค" : "";
270
- const title = titles[s.session_id]?.replace(/^\[PPM\]\s*/, "") || "";
271
- const preview = title ? ` โ€” ${escapeHtml(title.slice(0, 40))}` : "";
293
+ if (pageItems.length === 0) {
294
+ await this.telegram!.sendMessage(Number(chatId), `No sessions on page ${page}.`);
295
+ return;
296
+ }
297
+
298
+ const header = project ? escapeHtml(project) : "All Projects";
299
+ let text = `<b>Sessions โ€” ${header}</b>`;
300
+ if (totalPages > 1) text += ` <i>(${page}/${totalPages})</i>`;
301
+ text += "\n\n";
302
+
303
+ pageItems.forEach((s, i) => {
304
+ const pin = pinnedIds.has(s.session_id) ? "๐Ÿ“Œ " : "";
305
+ const activeDot = s.is_active ? " โฌค" : "";
306
+ const rawTitle = titles[s.session_id]?.replace(/^\[PPM\]\s*/, "") || "";
307
+ const title = rawTitle
308
+ ? escapeHtml(rawTitle.slice(0, 45))
309
+ : "<i>untitled</i>";
272
310
  const date = new Date(s.last_message_at * 1000).toLocaleString(undefined, {
273
311
  month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
274
312
  });
275
313
  const sid = s.session_id.slice(0, 8);
276
- text += `${i + 1}. <b>${escapeHtml(s.project_name)}</b>${preview}${active}\n`;
314
+ const num = start + i + 1;
315
+
316
+ text += `${pin}${num}. ${title}${activeDot}\n`;
277
317
  text += ` <code>${sid}</code> ยท ${date}\n\n`;
278
318
  });
279
- text += "Resume: /resume &lt;number&gt;";
319
+
320
+ text += "Resume: /resume &lt;n&gt; or /resume &lt;id&gt;";
321
+ if (totalPages > 1 && page < totalPages) {
322
+ text += `\nNext: /sessions ${page + 1}`;
323
+ }
324
+
280
325
  await this.telegram!.sendMessage(Number(chatId), text);
281
326
  }
282
327
 
@@ -427,8 +472,8 @@ class PPMBotService {
427
472
  /start โ€” Greeting + list projects
428
473
  /project &lt;name&gt; โ€” Switch/list projects
429
474
  /new โ€” Fresh session (current project)
430
- /sessions โ€” List recent sessions
431
- /resume &lt;n&gt; โ€” Resume session #n
475
+ /sessions [page] โ€” List sessions (current project)
476
+ /resume &lt;n or id&gt; โ€” Resume session
432
477
  /status โ€” Current project/session info
433
478
  /stop โ€” End current session
434
479
  /memory โ€” Show project memories