chat-logbook 0.14.0 → 0.15.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 +1 -1
- package/api/dist/index.js +76 -84
- package/package.json +1 -1
- package/web/dist/assets/index-CSvjgIUr.js +57 -0
- package/web/dist/assets/index-DcsvYh-v.css +2 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-BQlthoCZ.css +0 -2
- package/web/dist/assets/index-zW6L5xEX.js +0 -57
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ The full problem statement, user stories, and direction live in the [PRD](docs/P
|
|
|
44
44
|
- **Live updates.** While Claude Code is actively writing to a chat, new messages appear in chat-logbook within seconds — no restart needed. New and changed chats surface in the list on their own, without a manual refresh.
|
|
45
45
|
- **Chat metadata at a glance.** A ⓘ button on the conversation header opens a popover showing when the chat started, when it was last updated, the agent, the project working directory, and the chat's IDs — with one-click copy.
|
|
46
46
|
- **Tags.** Create tags, give them colors, and assign them to chats. They show as colored chips on the chat and in the list. Rename or delete them anytime.
|
|
47
|
-
- **Filter by tag or project.** Narrow the chat list to a project, or to chats
|
|
47
|
+
- **Filter by tag or project.** Narrow the chat list to a project, or to chats by tag — match every tag you pick, or any one of them.
|
|
48
48
|
|
|
49
49
|
## Quick start
|
|
50
50
|
|
package/api/dist/index.js
CHANGED
|
@@ -3088,6 +3088,7 @@ import Database from "better-sqlite3";
|
|
|
3088
3088
|
function buildFilterClauses({
|
|
3089
3089
|
projects,
|
|
3090
3090
|
tags: tags3,
|
|
3091
|
+
tagMode = "all",
|
|
3091
3092
|
hasMetadata
|
|
3092
3093
|
}) {
|
|
3093
3094
|
const clauses = [];
|
|
@@ -3100,21 +3101,30 @@ function buildFilterClauses({
|
|
|
3100
3101
|
if (tags3 && tags3.length > 0) {
|
|
3101
3102
|
const realTagIds = tags3.filter((t) => t !== "");
|
|
3102
3103
|
const wantUntagged = tags3.includes("");
|
|
3104
|
+
let realFragment = null;
|
|
3103
3105
|
if (realTagIds.length > 0) {
|
|
3104
3106
|
if (!hasMetadata) {
|
|
3105
|
-
|
|
3107
|
+
realFragment = "0";
|
|
3106
3108
|
} else {
|
|
3107
3109
|
const placeholders = realTagIds.map(() => "?").join(", ");
|
|
3108
|
-
|
|
3109
|
-
`c.id IN (SELECT chat_id FROM meta.chat_tags
|
|
3110
|
+
if (tagMode === "any") {
|
|
3111
|
+
realFragment = `c.id IN (SELECT DISTINCT chat_id FROM meta.chat_tags
|
|
3112
|
+
WHERE tag_id IN (${placeholders}))`;
|
|
3113
|
+
params.push(...realTagIds);
|
|
3114
|
+
} else {
|
|
3115
|
+
realFragment = `c.id IN (SELECT chat_id FROM meta.chat_tags
|
|
3110
3116
|
WHERE tag_id IN (${placeholders})
|
|
3111
|
-
GROUP BY chat_id HAVING count(*) = ?)
|
|
3112
|
-
|
|
3113
|
-
|
|
3117
|
+
GROUP BY chat_id HAVING count(*) = ?)`;
|
|
3118
|
+
params.push(...realTagIds, realTagIds.length);
|
|
3119
|
+
}
|
|
3114
3120
|
}
|
|
3115
3121
|
}
|
|
3116
|
-
|
|
3117
|
-
|
|
3122
|
+
const untaggedFragment = wantUntagged && hasMetadata ? "c.id NOT IN (SELECT chat_id FROM meta.chat_tags)" : null;
|
|
3123
|
+
if (tagMode === "any" && realFragment && untaggedFragment) {
|
|
3124
|
+
clauses.push(`(${realFragment} OR ${untaggedFragment})`);
|
|
3125
|
+
} else {
|
|
3126
|
+
if (realFragment) clauses.push(realFragment);
|
|
3127
|
+
if (untaggedFragment) clauses.push(untaggedFragment);
|
|
3118
3128
|
}
|
|
3119
3129
|
}
|
|
3120
3130
|
return { clauses, params };
|
|
@@ -3188,6 +3198,7 @@ function createChatPageQuery({
|
|
|
3188
3198
|
const filter = buildFilterClauses({
|
|
3189
3199
|
projects: query.projects,
|
|
3190
3200
|
tags: query.tags,
|
|
3201
|
+
tagMode: query.tagMode,
|
|
3191
3202
|
hasMetadata
|
|
3192
3203
|
});
|
|
3193
3204
|
clauses.push(...filter.clauses);
|
|
@@ -3251,34 +3262,6 @@ function createChatReader({
|
|
|
3251
3262
|
function key(agent, sourceId) {
|
|
3252
3263
|
return [agent, sourceId].join("\0");
|
|
3253
3264
|
}
|
|
3254
|
-
function listChats({
|
|
3255
|
-
includeTrashed,
|
|
3256
|
-
projects,
|
|
3257
|
-
tags: tagSelection
|
|
3258
|
-
}) {
|
|
3259
|
-
const visibility = loadChatVisibility(metadata2, { includeTrashed });
|
|
3260
|
-
const rows = archive2.read.listChatRows(projects ? { projects } : void 0);
|
|
3261
|
-
let passesTagFilter = null;
|
|
3262
|
-
if (tagSelection) {
|
|
3263
|
-
const realTagIds = tagSelection.filter((t) => t !== "");
|
|
3264
|
-
const wantUntagged = tagSelection.includes("");
|
|
3265
|
-
const allowedByTags = realTagIds.length > 0 ? new Set(tags3.listChatIdsWithAllTags(realTagIds)) : null;
|
|
3266
|
-
const taggedChatIds = wantUntagged ? new Set(tags3.listTagsByChat().keys()) : null;
|
|
3267
|
-
passesTagFilter = (chatInternalId) => {
|
|
3268
|
-
if (allowedByTags && !allowedByTags.has(chatInternalId)) return false;
|
|
3269
|
-
if (taggedChatIds && taggedChatIds.has(chatInternalId)) return false;
|
|
3270
|
-
return true;
|
|
3271
|
-
};
|
|
3272
|
-
}
|
|
3273
|
-
const toResponse = loadHydration();
|
|
3274
|
-
const chats2 = [];
|
|
3275
|
-
for (const row of rows) {
|
|
3276
|
-
if (!visibility.isVisible(row.id)) continue;
|
|
3277
|
-
if (passesTagFilter && !passesTagFilter(row.id)) continue;
|
|
3278
|
-
chats2.push(toResponse(row, visibility));
|
|
3279
|
-
}
|
|
3280
|
-
return chats2;
|
|
3281
|
-
}
|
|
3282
3265
|
function loadHydration(scope) {
|
|
3283
3266
|
const tsRangeByKey = new Map(
|
|
3284
3267
|
archive2.read.listChatTsRanges(scope).map((r) => [key(r.agent, r.sourceId), r])
|
|
@@ -3323,7 +3306,8 @@ function createChatReader({
|
|
|
3323
3306
|
includeTrashed = false,
|
|
3324
3307
|
trashedOnly = false,
|
|
3325
3308
|
projects,
|
|
3326
|
-
tags: tagSelection
|
|
3309
|
+
tags: tagSelection,
|
|
3310
|
+
tagMode
|
|
3327
3311
|
}) {
|
|
3328
3312
|
if (!pageQuery2) {
|
|
3329
3313
|
throw new Error("listChatsPage requires a pageQuery dependency");
|
|
@@ -3337,7 +3321,8 @@ function createChatReader({
|
|
|
3337
3321
|
includeTrashed,
|
|
3338
3322
|
trashedOnly,
|
|
3339
3323
|
projects,
|
|
3340
|
-
tags: tagSelection
|
|
3324
|
+
tags: tagSelection,
|
|
3325
|
+
tagMode
|
|
3341
3326
|
});
|
|
3342
3327
|
const visibility = loadChatVisibility(metadata2, {
|
|
3343
3328
|
includeTrashed: includeTrashed || trashedOnly
|
|
@@ -3369,12 +3354,18 @@ function createChatReader({
|
|
|
3369
3354
|
function listFilteredTotal({
|
|
3370
3355
|
includeTrashed = false,
|
|
3371
3356
|
projects,
|
|
3372
|
-
tags: tags4
|
|
3357
|
+
tags: tags4,
|
|
3358
|
+
tagMode
|
|
3373
3359
|
}) {
|
|
3374
3360
|
if (!countsQuery2) {
|
|
3375
3361
|
throw new Error("listFilteredTotal requires a countsQuery dependency");
|
|
3376
3362
|
}
|
|
3377
|
-
return countsQuery2.queryFilteredTotal({
|
|
3363
|
+
return countsQuery2.queryFilteredTotal({
|
|
3364
|
+
includeTrashed,
|
|
3365
|
+
projects,
|
|
3366
|
+
tags: tags4,
|
|
3367
|
+
tagMode
|
|
3368
|
+
});
|
|
3378
3369
|
}
|
|
3379
3370
|
function getMessages(id, { includeTrashed }) {
|
|
3380
3371
|
const row = findChat(id);
|
|
@@ -3389,7 +3380,6 @@ function createChatReader({
|
|
|
3389
3380
|
}));
|
|
3390
3381
|
}
|
|
3391
3382
|
return {
|
|
3392
|
-
listChats,
|
|
3393
3383
|
listChatsPage,
|
|
3394
3384
|
listCounts,
|
|
3395
3385
|
listFilteredTotal,
|
|
@@ -3423,47 +3413,44 @@ function createApp({
|
|
|
3423
3413
|
app2.get("/api/chats", (c) => {
|
|
3424
3414
|
const includeTrashed = c.req.query("includeTrashed") === "true";
|
|
3425
3415
|
const limitParam = c.req.query("limit");
|
|
3426
|
-
if (limitParam
|
|
3427
|
-
|
|
3428
|
-
return c.json({ error: "Pagination is not available" }, 501);
|
|
3429
|
-
}
|
|
3430
|
-
const limit = Number.parseInt(limitParam, 10);
|
|
3431
|
-
if (!Number.isInteger(limit) || limit <= 0 || limit > MAX_PAGE_LIMIT) {
|
|
3432
|
-
return c.json({ error: "Invalid limit" }, 400);
|
|
3433
|
-
}
|
|
3434
|
-
const sort = c.req.query("sort") ?? "updatedAt";
|
|
3435
|
-
if (sort !== "createdAt" && sort !== "updatedAt" && sort !== "deletedAt" && sort !== "title") {
|
|
3436
|
-
return c.json({ error: "Invalid sort" }, 400);
|
|
3437
|
-
}
|
|
3438
|
-
const direction = c.req.query("direction") ?? "desc";
|
|
3439
|
-
if (direction !== "asc" && direction !== "desc") {
|
|
3440
|
-
return c.json({ error: "Invalid direction" }, 400);
|
|
3441
|
-
}
|
|
3442
|
-
const trashedOnly = c.req.query("trashedOnly") === "true";
|
|
3443
|
-
const projects2 = c.req.queries("project");
|
|
3444
|
-
const tagsParam2 = c.req.query("tags");
|
|
3445
|
-
const tagSelection = tagsParam2 === void 0 ? void 0 : tagsParam2.split(",");
|
|
3446
|
-
const { chats: chats3, nextCursor } = reader.listChatsPage({
|
|
3447
|
-
sort,
|
|
3448
|
-
direction,
|
|
3449
|
-
limit,
|
|
3450
|
-
cursor: c.req.query("cursor"),
|
|
3451
|
-
includeTrashed,
|
|
3452
|
-
trashedOnly,
|
|
3453
|
-
projects: projects2,
|
|
3454
|
-
tags: tagSelection
|
|
3455
|
-
});
|
|
3456
|
-
return c.json({ chats: chats3, nextCursor });
|
|
3416
|
+
if (limitParam === void 0) {
|
|
3417
|
+
return c.json({ error: "Missing limit" }, 400);
|
|
3457
3418
|
}
|
|
3419
|
+
if (!pageQuery2) {
|
|
3420
|
+
return c.json({ error: "Pagination is not available" }, 501);
|
|
3421
|
+
}
|
|
3422
|
+
const limit = Number.parseInt(limitParam, 10);
|
|
3423
|
+
if (!Number.isInteger(limit) || limit <= 0 || limit > MAX_PAGE_LIMIT) {
|
|
3424
|
+
return c.json({ error: "Invalid limit" }, 400);
|
|
3425
|
+
}
|
|
3426
|
+
const sort = c.req.query("sort") ?? "updatedAt";
|
|
3427
|
+
if (sort !== "createdAt" && sort !== "updatedAt" && sort !== "deletedAt" && sort !== "title") {
|
|
3428
|
+
return c.json({ error: "Invalid sort" }, 400);
|
|
3429
|
+
}
|
|
3430
|
+
const direction = c.req.query("direction") ?? "desc";
|
|
3431
|
+
if (direction !== "asc" && direction !== "desc") {
|
|
3432
|
+
return c.json({ error: "Invalid direction" }, 400);
|
|
3433
|
+
}
|
|
3434
|
+
const trashedOnly = c.req.query("trashedOnly") === "true";
|
|
3458
3435
|
const projects = c.req.queries("project");
|
|
3459
3436
|
const tagsParam = c.req.query("tags");
|
|
3460
|
-
const
|
|
3461
|
-
const
|
|
3437
|
+
const tagSelection = tagsParam === void 0 ? void 0 : tagsParam.split(",");
|
|
3438
|
+
const tagMode = c.req.query("tagMode") ?? "all";
|
|
3439
|
+
if (tagMode !== "all" && tagMode !== "any") {
|
|
3440
|
+
return c.json({ error: "Invalid tagMode" }, 400);
|
|
3441
|
+
}
|
|
3442
|
+
const { chats: chats2, nextCursor } = reader.listChatsPage({
|
|
3443
|
+
sort,
|
|
3444
|
+
direction,
|
|
3445
|
+
limit,
|
|
3446
|
+
cursor: c.req.query("cursor"),
|
|
3462
3447
|
includeTrashed,
|
|
3448
|
+
trashedOnly,
|
|
3463
3449
|
projects,
|
|
3464
|
-
tags:
|
|
3450
|
+
tags: tagSelection,
|
|
3451
|
+
tagMode
|
|
3465
3452
|
});
|
|
3466
|
-
return c.json({ chats: chats2 });
|
|
3453
|
+
return c.json({ chats: chats2, nextCursor });
|
|
3467
3454
|
});
|
|
3468
3455
|
app2.get("/api/chats/stream", (c) => {
|
|
3469
3456
|
if (!listEvents2) {
|
|
@@ -3501,7 +3488,16 @@ function createApp({
|
|
|
3501
3488
|
const projects = c.req.queries("project");
|
|
3502
3489
|
const tagsParam = c.req.query("tags");
|
|
3503
3490
|
const tags4 = tagsParam === void 0 ? void 0 : tagsParam.split(",");
|
|
3504
|
-
const
|
|
3491
|
+
const tagMode = c.req.query("tagMode") ?? "all";
|
|
3492
|
+
if (tagMode !== "all" && tagMode !== "any") {
|
|
3493
|
+
return c.json({ error: "Invalid tagMode" }, 400);
|
|
3494
|
+
}
|
|
3495
|
+
const total = reader.listFilteredTotal({
|
|
3496
|
+
includeTrashed,
|
|
3497
|
+
projects,
|
|
3498
|
+
tags: tags4,
|
|
3499
|
+
tagMode
|
|
3500
|
+
});
|
|
3505
3501
|
return c.json({ total });
|
|
3506
3502
|
});
|
|
3507
3503
|
app2.delete("/api/chats/:id", (c) => {
|
|
@@ -9895,11 +9891,6 @@ function createTagRepository({
|
|
|
9895
9891
|
byChat.set(row.chatId, list);
|
|
9896
9892
|
}
|
|
9897
9893
|
return byChat;
|
|
9898
|
-
},
|
|
9899
|
-
listChatIdsWithAllTags(tagIds) {
|
|
9900
|
-
const distinct = [...new Set(tagIds)];
|
|
9901
|
-
if (distinct.length === 0) return [];
|
|
9902
|
-
return db.select({ chatId: chatTags.chatId }).from(chatTags).where(inArray(chatTags.tagId, distinct)).groupBy(chatTags.chatId).having(sql`count(${chatTags.tagId}) = ${distinct.length}`).all().map((r) => r.chatId);
|
|
9903
9894
|
}
|
|
9904
9895
|
};
|
|
9905
9896
|
}
|
|
@@ -12045,7 +12036,8 @@ function createChatCountsQuery({
|
|
|
12045
12036
|
function queryFilteredTotal({
|
|
12046
12037
|
includeTrashed = false,
|
|
12047
12038
|
projects,
|
|
12048
|
-
tags: tags3
|
|
12039
|
+
tags: tags3,
|
|
12040
|
+
tagMode = "all"
|
|
12049
12041
|
}) {
|
|
12050
12042
|
const clauses = [];
|
|
12051
12043
|
const params = [];
|
|
@@ -12056,7 +12048,7 @@ function createChatCountsQuery({
|
|
|
12056
12048
|
} else if (includeTrashed) {
|
|
12057
12049
|
return 0;
|
|
12058
12050
|
}
|
|
12059
|
-
const filter = buildFilterClauses({ projects, tags: tags3, hasMetadata });
|
|
12051
|
+
const filter = buildFilterClauses({ projects, tags: tags3, tagMode, hasMetadata });
|
|
12060
12052
|
clauses.push(...filter.clauses);
|
|
12061
12053
|
params.push(...filter.params);
|
|
12062
12054
|
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|