chat-logbook 0.8.0 → 0.10.0

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
@@ -60,6 +60,9 @@ The full problem statement, user stories, and direction live in the
60
60
  Code's terminal look.
61
61
  - **Soft delete with Trash.** Hide chats you don't want to see; restore
62
62
  them anytime.
63
+ - **Sortable lists.** Sort your chats by title, created time, or updated
64
+ time, and the choice sticks between visits. Trash sorts independently —
65
+ by deleted time by default.
63
66
  - **Custom chat titles.** Rename any chat — click its title in the
64
67
  list or conversation header, or select it and press `F2` / `↵`. Clear
65
68
  the title to fall back to the first message.
@@ -69,6 +72,10 @@ The full problem statement, user stories, and direction live in the
69
72
  - **Live updates.** While Claude Code is actively writing to a chat,
70
73
  new messages appear in chat-logbook within seconds — no restart
71
74
  needed.
75
+ - **Chat metadata at a glance.** A ⓘ button on the conversation
76
+ header opens a popover showing when the chat started, when it was
77
+ last updated, the agent, the project working directory, and the
78
+ chat's IDs — with one-click copy.
72
79
 
73
80
  ## Quick start
74
81
 
@@ -0,0 +1,2 @@
1
+ ALTER TABLE `chats_meta` ADD `deleted_at` integer;--> statement-breakpoint
2
+ UPDATE `chats_meta` SET `deleted_at` = `updated_at` WHERE `is_deleted` = 1;
@@ -0,0 +1 @@
1
+ ALTER TABLE `chats` ADD `project_path` text;
@@ -43,6 +43,13 @@
43
43
  "when": 1779000000000,
44
44
  "tag": "0005_rename_session_to_chat",
45
45
  "breakpoints": true
46
+ },
47
+ {
48
+ "idx": 6,
49
+ "version": "6",
50
+ "when": 1779100000000,
51
+ "tag": "0006_add_project_path",
52
+ "breakpoints": true
46
53
  }
47
54
  ]
48
55
  }
@@ -22,6 +22,13 @@
22
22
  "when": 1779000000000,
23
23
  "tag": "0002_rename_sessions_meta_to_chats_meta",
24
24
  "breakpoints": true
25
+ },
26
+ {
27
+ "idx": 3,
28
+ "version": "6",
29
+ "when": 1779500000000,
30
+ "tag": "0003_add_deleted_at",
31
+ "breakpoints": true
25
32
  }
26
33
  ]
27
34
  }
package/api/dist/index.js CHANGED
@@ -8187,7 +8187,8 @@ var chats = sqliteTable(
8187
8187
  agent: text("agent").notNull(),
8188
8188
  sourceId: text("source_id").notNull(),
8189
8189
  firstSeenAt: integer("first_seen_at", { mode: "timestamp_ms" }).notNull(),
8190
- project: text("project")
8190
+ project: text("project"),
8191
+ projectPath: text("project_path")
8191
8192
  },
8192
8193
  (t) => [uniqueIndex("chats_agent_source_idx").on(t.agent, t.sourceId)]
8193
8194
  );
@@ -8251,11 +8252,16 @@ var ingestionEvents = sqliteTable("ingestion_events", {
8251
8252
 
8252
8253
  // src/visibility.ts
8253
8254
  function loadChatVisibility(metadata2, opts) {
8254
- const trashed = new Set(metadata2.listDeletedIds());
8255
+ const deleted = metadata2.listDeleted();
8256
+ const trashed = new Set(deleted.map((r) => r.id));
8257
+ const deletedAtById = new Map(
8258
+ deleted.map((r) => [r.id, r.deletedAt?.getTime() ?? null])
8259
+ );
8255
8260
  const showTrashed = opts.includeTrashed === true;
8256
8261
  return {
8257
8262
  isTrashed: (internalId) => trashed.has(internalId),
8258
- isVisible: (internalId) => showTrashed || !trashed.has(internalId)
8263
+ isVisible: (internalId) => showTrashed || !trashed.has(internalId),
8264
+ deletedAt: (internalId) => deletedAtById.get(internalId) ?? null
8259
8265
  };
8260
8266
  }
8261
8267
 
@@ -8285,18 +8291,33 @@ function createApp({ archive: archive2, metadata: metadata2, webDistDir: webDist
8285
8291
  for (const row of rows) {
8286
8292
  if (!visibility.isVisible(row.id)) continue;
8287
8293
  const isDeleted = visibility.isTrashed(row.id);
8288
- const lastMessage = archive2.db.select({ ts: messages.ts }).from(messages).where(
8294
+ const latestRaw = archive2.db.select({ sourcePath: rawMessages.sourcePath }).from(rawMessages).where(
8295
+ and(
8296
+ eq(rawMessages.agent, CLAUDE_CODE_AGENT),
8297
+ eq(rawMessages.sourceId, row.sourceId)
8298
+ )
8299
+ ).orderBy(desc(rawMessages.ingestedAt)).limit(1).get();
8300
+ const tsRange = archive2.db.select({
8301
+ minTs: sql`min(${messages.ts})`,
8302
+ maxTs: sql`max(${messages.ts})`
8303
+ }).from(messages).where(
8289
8304
  and(
8290
8305
  eq(messages.agent, CLAUDE_CODE_AGENT),
8291
8306
  eq(messages.sourceId, row.sourceId)
8292
8307
  )
8293
- ).orderBy(desc(messages.ts)).limit(1).get();
8308
+ ).get();
8309
+ const firstSeenAtMs = row.firstSeenAt.getTime();
8294
8310
  const chat = {
8295
8311
  id: row.sourceId,
8312
+ chatId: row.chatId,
8313
+ agent: row.agent,
8296
8314
  title: deriveTitle(archive2, metadata2, row),
8297
8315
  project: row.project ?? "",
8298
- createdAt: row.firstSeenAt.getTime(),
8299
- updatedAt: lastMessage ? lastMessage.ts.getTime() : row.firstSeenAt.getTime()
8316
+ projectPath: row.projectPath ?? null,
8317
+ sourceFilePath: latestRaw?.sourcePath ?? null,
8318
+ createdAt: tsRange?.minTs ?? firstSeenAtMs,
8319
+ updatedAt: tsRange?.maxTs ?? firstSeenAtMs,
8320
+ deletedAt: visibility.deletedAt(row.id)
8300
8321
  };
8301
8322
  if (isDeleted) chat.isDeleted = true;
8302
8323
  chats2.push(chat);
@@ -8685,7 +8706,10 @@ var chatsMeta = sqliteTable("chats_meta", {
8685
8706
  isDeleted: integer("is_deleted", { mode: "boolean" }).notNull().default(false),
8686
8707
  customTitle: text("custom_title"),
8687
8708
  createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
8688
- updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull()
8709
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
8710
+ // Set when a chat is moved to Trash; null while active. Drives the Trash
8711
+ // view's independent "Deleted time" sort axis.
8712
+ deletedAt: integer("deleted_at", { mode: "timestamp_ms" })
8689
8713
  });
8690
8714
 
8691
8715
  // src/metadata/repository.ts
@@ -8721,23 +8745,28 @@ function createMetadataRepository({
8721
8745
  id: internalId,
8722
8746
  isDeleted: true,
8723
8747
  createdAt: now,
8724
- updatedAt: now
8748
+ updatedAt: now,
8749
+ deletedAt: now
8725
8750
  }).onConflictDoUpdate({
8726
8751
  target: chatsMeta.id,
8727
- set: { isDeleted: true, updatedAt: now }
8752
+ set: { isDeleted: true, updatedAt: now, deletedAt: now }
8728
8753
  }).run();
8729
8754
  },
8730
8755
  restore(internalId) {
8731
8756
  const now = /* @__PURE__ */ new Date();
8732
- db.update(chatsMeta).set({ isDeleted: false, updatedAt: now }).where(eq(chatsMeta.id, internalId)).run();
8757
+ db.update(chatsMeta).set({ isDeleted: false, updatedAt: now, deletedAt: null }).where(eq(chatsMeta.id, internalId)).run();
8733
8758
  },
8734
8759
  isDeleted(internalId) {
8735
8760
  const row = db.select({ isDeleted: chatsMeta.isDeleted }).from(chatsMeta).where(eq(chatsMeta.id, internalId)).get();
8736
8761
  return row?.isDeleted ?? false;
8737
8762
  },
8738
- listDeletedIds() {
8739
- const rows = db.select({ id: chatsMeta.id }).from(chatsMeta).where(eq(chatsMeta.isDeleted, true)).all();
8740
- return rows.map((r) => r.id);
8763
+ getDeletedAt(internalId) {
8764
+ const row = db.select({ deletedAt: chatsMeta.deletedAt }).from(chatsMeta).where(eq(chatsMeta.id, internalId)).get();
8765
+ return row?.deletedAt ?? null;
8766
+ },
8767
+ listDeleted() {
8768
+ const rows = db.select({ id: chatsMeta.id, deletedAt: chatsMeta.deletedAt }).from(chatsMeta).where(eq(chatsMeta.isDeleted, true)).all();
8769
+ return rows.map((r) => ({ id: r.id, deletedAt: r.deletedAt ?? null }));
8741
8770
  },
8742
8771
  getCustomTitle(internalId) {
8743
8772
  const row = db.select({ customTitle: chatsMeta.customTitle }).from(chatsMeta).where(eq(chatsMeta.id, internalId)).get();
@@ -8795,7 +8824,14 @@ async function runIngestion(opts) {
8795
8824
  eq(chatScanState.sourceId, ref.sourceId)
8796
8825
  )
8797
8826
  ).get();
8798
- ensureChat(opts.archive, plugin.id, ref.sourceId, now(), ref.project);
8827
+ ensureChat(
8828
+ opts.archive,
8829
+ plugin.id,
8830
+ ref.sourceId,
8831
+ now(),
8832
+ ref.project,
8833
+ ref.projectPath
8834
+ );
8799
8835
  if (stat4 && prior && prior.lastMtimeMs === stat4.mtimeMs && prior.lastSizeBytes === stat4.size) {
8800
8836
  result.skippedByMtime += 1;
8801
8837
  continue;
@@ -8880,11 +8916,16 @@ function recordScanState(archive2, agent, sourceId, sourcePath, stat4, scannedAt
8880
8916
  }).run();
8881
8917
  }
8882
8918
  }
8883
- function ensureChat(archive2, agent, sourceId, firstSeenAt, project) {
8919
+ function ensureChat(archive2, agent, sourceId, firstSeenAt, project, projectPath) {
8884
8920
  const existing = archive2.db.select().from(chats).where(and(eq(chats.agent, agent), eq(chats.sourceId, sourceId))).get();
8885
8921
  if (existing) {
8886
- if (project && existing.project !== project) {
8887
- archive2.db.update(chats).set({ project }).where(eq(chats.id, existing.id)).run();
8922
+ const updates = {};
8923
+ if (project && existing.project !== project) updates.project = project;
8924
+ if (projectPath && existing.projectPath !== projectPath) {
8925
+ updates.projectPath = projectPath;
8926
+ }
8927
+ if (Object.keys(updates).length > 0) {
8928
+ archive2.db.update(chats).set(updates).where(eq(chats.id, existing.id)).run();
8888
8929
  }
8889
8930
  return existing.id;
8890
8931
  }
@@ -8896,7 +8937,8 @@ function ensureChat(archive2, agent, sourceId, firstSeenAt, project) {
8896
8937
  agent,
8897
8938
  sourceId,
8898
8939
  firstSeenAt,
8899
- project: project ?? null
8940
+ project: project ?? null,
8941
+ projectPath: projectPath ?? null
8900
8942
  }).run();
8901
8943
  return id;
8902
8944
  }
@@ -10786,7 +10828,8 @@ var ClaudeCodePlugin = class {
10786
10828
  sourceId,
10787
10829
  sourcePath,
10788
10830
  watchPaths: [sourcePath],
10789
- project: cwd ? path3.basename(cwd) : void 0
10831
+ project: cwd ? path3.basename(cwd) : void 0,
10832
+ projectPath: cwd ?? void 0
10790
10833
  };
10791
10834
  }
10792
10835
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chat-logbook",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "A local-first conversation manager for Claude Code",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",