mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-26 00:46:23 +02:00
show chat titles
This commit is contained in:
parent
146c3d66bd
commit
2683d4e371
4 changed files with 108 additions and 16 deletions
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue