chat history in projects

This commit is contained in:
2026-02-23 06:49:18 +00:00
parent 46c3b6b71d
commit 3e2c8090d5
8 changed files with 212 additions and 30 deletions
+24 -18
View File
@@ -4,6 +4,7 @@ import * as storage from './storage';
import { readApiKeys, readLocalProviders } from '../server-settings/pi-mono';
import { listPiModels } from './list-models';
import { getHomeDir } from '../../data-path';
import { resolveBaseCwd } from './websocket';
import { logger } from './logger';
/**
@@ -38,7 +39,8 @@ piRestRouter.get('/pi/models', async (ctx: Context) => {
/**
* POST /api/pi/sessions
* List all sessions for the current user
* List all sessions for the current user.
* Optionally filter by cwd/cwdRoot to show only project-scoped sessions.
*/
piRestRouter.post('/pi/sessions', async (ctx: Context) => {
const user = ctx.get('user');
@@ -46,11 +48,15 @@ piRestRouter.post('/pi/sessions', async (ctx: Context) => {
return ctx.json({ error: 'Unauthorized' }, 401);
}
// TODO: Implement proper user home directory resolution
const body = await ctx.req.json().catch(() => ({}));
const userHome = getHomeDir(user.email);
const filterCwd = body.cwd ? resolveBaseCwd(user.email, body.cwdRoot, body.cwd) : null;
try {
const sessions = await storage.listUserSessions(userHome);
let sessions = await storage.listUserSessions(userHome);
if (filterCwd) {
sessions = sessions.filter((s) => s.cwd === filterCwd);
}
return ctx.json({ sessions });
} catch (err) {
logger.error('Failed to list sessions', { email: ctx.get('email'), error: String(err) });
@@ -84,7 +90,7 @@ piRestRouter.get('/pi/sessions/:sessionId', async (ctx: Context) => {
// Not in root, search in groups
const groups = await storage.listGroups(userHome);
let found = false;
for (const group of groups) {
try {
({ meta, messages } = await storage.loadSession(userHome, sessionId, group.slug));
@@ -94,7 +100,7 @@ piRestRouter.get('/pi/sessions/:sessionId', async (ctx: Context) => {
continue;
}
}
if (!found) {
throw new Error('Session not found');
}
@@ -137,7 +143,7 @@ piRestRouter.patch('/pi/sessions/:sessionId', async (ctx: Context) => {
try {
// Find the session (root or in group)
let groupSlug: string | null = null;
try {
const { meta } = await storage.loadSession(userHome, sessionId);
groupSlug = meta.groupSlug || null;
@@ -158,8 +164,8 @@ piRestRouter.patch('/pi/sessions/:sessionId', async (ctx: Context) => {
const updatedMeta = await storage.updateSessionMeta(userHome, sessionId, {
title: body.title,
}, groupSlug);
return ctx.json({
return ctx.json({
success: true,
session: updatedMeta,
});
@@ -189,7 +195,7 @@ piRestRouter.delete('/pi/sessions/:sessionId', async (ctx: Context) => {
try {
// Find the session (root or in group)
let groupSlug: string | null = null;
try {
const { meta } = await storage.loadSession(userHome, sessionId);
groupSlug = meta.groupSlug || null;
@@ -208,7 +214,7 @@ piRestRouter.delete('/pi/sessions/:sessionId', async (ctx: Context) => {
}
await storage.deleteSession(userHome, sessionId, groupSlug);
// Update group session count if in a group
if (groupSlug) {
try {
@@ -220,7 +226,7 @@ piRestRouter.delete('/pi/sessions/:sessionId', async (ctx: Context) => {
// Group might not exist anymore
}
}
return ctx.json({ success: true });
} catch (err) {
logger.error('Failed to delete session', { sessionId, email: ctx.get('email'), error: String(err) });
@@ -265,11 +271,11 @@ piRestRouter.post('/pi/groups', async (ctx: Context) => {
}
const body = await ctx.req.json();
if (!body.name || typeof body.name !== 'string') {
return ctx.json({ error: 'Name is required and must be a string' }, 400);
}
if (!body.slug || typeof body.slug !== 'string') {
return ctx.json({ error: 'Slug is required and must be a string' }, 400);
}
@@ -319,7 +325,7 @@ piRestRouter.post('/pi/groups', async (ctx: Context) => {
await storage.saveGroup(userHome, groupMeta);
}
return ctx.json({
return ctx.json({
success: true,
group: groupMeta,
});
@@ -366,8 +372,8 @@ piRestRouter.patch('/pi/groups/:groupSlug', async (ctx: Context) => {
}
const body = await ctx.req.json();
const updates: any = {};
const updates: Record<string, string> = {};
if (body.name && typeof body.name === 'string') {
updates.name = body.name;
}
@@ -383,7 +389,7 @@ piRestRouter.patch('/pi/groups/:groupSlug', async (ctx: Context) => {
try {
const updatedGroup = await storage.updateGroupMeta(userHome, groupSlug, updates);
return ctx.json({
return ctx.json({
success: true,
group: updatedGroup,
});
@@ -482,7 +488,7 @@ piRestRouter.post('/pi/sessions/:sessionId/move', async (ctx: Context) => {
const updatedMeta = await storage.moveSession(userHome, sessionId, fromGroupSlug, toGroupSlug);
return ctx.json({
return ctx.json({
success: true,
session: updatedMeta,
});
+3
View File
@@ -46,6 +46,7 @@ export type ClientMessage =
sessionId?: string;
model?: string;
cwd?: string;
cwdRoot?: string;
sandboxed?: boolean;
groupSlug?: string;
attachmentIds?: string[];
@@ -53,6 +54,8 @@ export type ClientMessage =
| {
type: "resume";
sessionId: string;
cwd?: string;
cwdRoot?: string;
}
| {
type: "stop";
+12 -7
View File
@@ -50,6 +50,11 @@ const resolveCwd = (home: string, cwd?: string) => {
return home;
};
export const resolveBaseCwd = (email: string, cwdRoot?: string, cwd?: string) => {
const root = resolveRoot(email, cwdRoot);
return resolveCwd(root, cwd);
};
const wsToSessionMap = new WeakMap<any, string>();
function sendToClient(ws: ServerWebSocket<WSData> | null, msg: ServerMessage): void {
@@ -93,7 +98,7 @@ export function close(ws: ServerWebSocket<WSData>): void {
}
}
function createEventHandler(sessionId: string, model: string, cwd: string) {
function createEventHandler(sessionId: string, model: string, cwd: string, storageDir: string) {
return async (event: PiEvent): Promise<void> => {
const session = sessionManager.getSession(sessionId);
if (!session) return;
@@ -212,7 +217,7 @@ function createEventHandler(sessionId: string, model: string, cwd: string) {
// Save session to disk
try {
await storage.saveSession(cwd, sessionId, session.meta, session.messages);
await storage.saveSession(storageDir, sessionId, session.meta, session.messages);
logger.info('Session saved to disk', { sessionId, messageCount: session.messages.length });
} catch (err) {
logger.error('Failed to save session', { sessionId, error: String(err) });
@@ -280,7 +285,7 @@ async function handleChat(
sendToClient(ws, { type: 'session:init', sessionId, model, cwd });
if (!session.piProcess) {
try {
const onEvent = createEventHandler(sessionId, model, cwd);
const onEvent = createEventHandler(sessionId, model, cwd, homeDir);
session.piProcess = await piBridge.spawnPi(cwd, model, onEvent, sandboxed ? { userId, username, email, homeDir } : undefined);
logger.info('Spawned Pi process for session', { sessionId, model, cwd, sandboxed });
} catch (err) {
@@ -313,17 +318,17 @@ async function handleChat(
async function handleResume(
ws: ServerWebSocket<WSData>,
msg: { sessionId: string }
msg: { sessionId: string; cwd?: string; cwdRoot?: string }
): Promise<void> {
const { email } = ws.data;
const { sessionId } = msg;
try {
let session = sessionManager.getSession(sessionId);
if (!session) {
const homeDir = getHomeDir(email);
try {
const { meta, messages } = await storage.loadSession(homeDir, sessionId);
@@ -349,7 +354,7 @@ async function handleResume(
try {
const homeDir = getHomeDir(email);
const sandbox = session.sandboxed && session.userId ? { userId: session.userId, username: ws.data.username, email, homeDir } : undefined;
const onEvent = createEventHandler(sessionId, session.model, session.cwd);
const onEvent = createEventHandler(sessionId, session.model, session.cwd, homeDir);
session.piProcess = await piBridge.spawnPi(session.cwd, session.model, onEvent, sandbox);
logger.info('Spawned fresh Pi process for resumed session', { sessionId, model: session.model, sandboxed: session.sandboxed });
} catch (err) {