show chat titles

This commit is contained in:
Ramnique Singh 2026-01-20 16:36:36 +05:30
parent 146c3d66bd
commit 2683d4e371
4 changed files with 108 additions and 16 deletions

View file

@ -3,7 +3,9 @@ import { IMonotonicallyIncreasingIdGenerator } from "../application/lib/id-gen.j
import { WorkDir } from "../config/config.js";
import path from "path";
import fsp from "fs/promises";
import { Run, RunEvent, StartEvent, CreateRunOptions, ListRunsResponse } from "@x/shared/dist/runs.js";
import fs from "fs";
import readline from "readline";
import { Run, RunEvent, StartEvent, CreateRunOptions, ListRunsResponse, MessageEvent } from "@x/shared/dist/runs.js";
export interface IRunsRepo {
create(options: z.infer<typeof CreateRunOptions>): Promise<z.infer<typeof Run>>;
@ -24,6 +26,96 @@ export class FSRunsRepo implements IRunsRepo {
fsp.mkdir(path.join(WorkDir, 'runs'), { recursive: true });
}
private extractTitle(events: z.infer<typeof RunEvent>[]): string | undefined {
for (const event of events) {
if (event.type === 'message') {
const messageEvent = event as z.infer<typeof MessageEvent>;
if (messageEvent.message.role === 'user') {
const content = messageEvent.message.content;
if (typeof content === 'string' && content.trim()) {
// Truncate to 100 chars for display
const truncated = content.trim();
return truncated.length > 100 ? truncated.substring(0, 100) : truncated;
}
}
}
}
return undefined;
}
/**
* Read file line-by-line using streams, stopping early once we have
* the start event and title (or determine there's no title).
*/
private async readRunMetadata(filePath: string): Promise<{
start: z.infer<typeof StartEvent>;
title: string | undefined;
} | null> {
return new Promise((resolve) => {
const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
let start: z.infer<typeof StartEvent> | null = null;
let title: string | undefined;
let lineIndex = 0;
rl.on('line', (line) => {
const trimmed = line.trim();
if (!trimmed) return;
try {
if (lineIndex === 0) {
// First line should be the start event
start = StartEvent.parse(JSON.parse(trimmed));
} else {
// Subsequent lines - look for first user message or assistant response
const event = RunEvent.parse(JSON.parse(trimmed));
if (event.type === 'message') {
const msg = event.message;
if (msg.role === 'user') {
// Found first user message - use as title
const content = msg.content;
if (typeof content === 'string' && content.trim()) {
const truncated = content.trim();
title = truncated.length > 100 ? truncated.substring(0, 100) : truncated;
}
// Stop reading
rl.close();
stream.destroy();
return;
} else if (msg.role === 'assistant') {
// Assistant responded before any user message - no title
rl.close();
stream.destroy();
return;
}
}
}
lineIndex++;
} catch {
// Skip malformed lines
}
});
rl.on('close', () => {
if (start) {
resolve({ start, title });
} else {
resolve(null);
}
});
rl.on('error', () => {
resolve(null);
});
stream.on('error', () => {
rl.close();
resolve(null);
});
});
}
async appendEvents(runId: string, events: z.infer<typeof RunEvent>[]): Promise<void> {
await fsp.appendFile(
path.join(WorkDir, 'runs', `${runId}.jsonl`),
@ -58,8 +150,10 @@ export class FSRunsRepo implements IRunsRepo {
if (events.length === 0 || events[0].type !== 'start') {
throw new Error('Corrupt run data');
}
const title = this.extractTitle(events);
return {
id,
title,
createdAt: events[0].ts!,
agentId: events[0].agentName,
log: events,
@ -103,21 +197,16 @@ export class FSRunsRepo implements IRunsRepo {
for (const name of selected) {
const runId = name.slice(0, -'.jsonl'.length);
try {
const contents = await fsp.readFile(path.join(runsDir, name), 'utf8');
const firstLine = contents.split('\n').find(line => line.trim() !== '');
if (!firstLine) {
continue;
}
const start = StartEvent.parse(JSON.parse(firstLine));
runs.push({
id: runId,
createdAt: start.ts!,
agentId: start.agentName,
});
} catch {
const metadata = await this.readRunMetadata(path.join(runsDir, name));
if (!metadata) {
continue;
}
runs.push({
id: runId,
title: metadata.title,
createdAt: metadata.start.ts!,
agentId: metadata.start.agentName,
});
}
const hasMore = startIndex + PAGE_SIZE < files.length;