* Unified session messages endpoint (PilotDeck-only).
*
* GET /api/sessions/:sessionId/messages?projectName=&projectPath=&limit=&offset=
*
* Reads transcripts through the gateway's `readSessionMessages` RPC.
* Previously this route imported `readWebSessionMessages` directly from
* `dist/src/web/server/` — that coupled `ui/server/` to compiled
* artifacts and meant `src/` edits were silently invisible until a
* `npm run build`. Going through the gateway WebSocket means the
* standalone `pilotdeck server` process owns the read path and we pick
* up its in-flight session writes automatically.
*
* @module routes/messages
*/
import express from 'express';
import { getPilotDeckGateway } from '../pilotdeck-bridge.js';
import { createNormalizedMessage } from '../pilotdeck-message.js';
const router = express.Router();
const REPO_ROOT = process.cwd();
router.get('/:sessionId/messages', async (req, res) => {
try {
const { sessionId } = req.params;
const projectPath = String(req.query.projectPath || req.query.projectName || REPO_ROOT);
const limitParam = req.query.limit;
const limit = limitParam !== undefined && limitParam !== null && limitParam !== ''
? parseInt(limitParam, 10)
: null;
const offset = parseInt(req.query.offset || '0', 10);
const gateway = await getPilotDeckGateway();
const result = await gateway.readSessionMessages({
sessionKey: sessionId,
projectKey: projectPath,
limit: limit ?? undefined,
cursor: offset > 0 ? String(offset) : undefined,
});
const messages = result.messages.map((message) => mapWebMessageToNormalized(message, sessionId));
const totalKnown = typeof result.total === 'number' ? result.total : messages.length + offset;
const hasMore = result.nextCursor !== undefined && result.nextCursor !== null;
return res.json({
messages,
total: totalKnown,
hasMore,
offset,
limit,
});
} catch (error) {
console.error('[messages] read_session_messages failed:', error);
return res.json({ messages: [], total: 0, hasMore: false, offset: 0, limit: null });
}
});
function mapWebMessageToNormalized(message, sessionId) {
const base = {
id: message.id,
sessionId,
timestamp: message.createdAt,
provider: message.provider || 'pilotdeck',
};
switch (message.kind) {
case 'text':
return createNormalizedMessage({
...base,
kind: 'text',
role: message.role === 'user' ? 'user' : 'assistant',
content: message.text || '',
...(Array.isArray(message.images) && message.images.length > 0
? { images: message.images.map((image) => image?.data).filter(Boolean) }
: {}),
});
case 'thinking':
return createNormalizedMessage({ ...base, kind: 'thinking', content: message.text || '' });
case 'tool_use':
return createNormalizedMessage({
...base,
kind: 'tool_use',
toolName: message.toolName,
toolInput: message.payload,
toolId: message.toolCallId,
});
case 'tool_result': {
const planPayload = message.payload && typeof message.payload === 'object'
? message.payload
: {};
return createNormalizedMessage({
...base,
kind: 'tool_result',
toolId: message.toolCallId,
content: message.text || '',
isError: message.ok === false,
...(message.errorCode ? { errorCode: message.errorCode } : {}),
...(Array.isArray(message.images) && message.images.length > 0
? {
toolResultImages: message.images
.filter((image) => image && typeof image.data === 'string')
.map((image) => ({ data: image.data, mimeType: image.mimeType })),
}
: {}),
...(planPayload.planFilePath ? {
planFilePath: planPayload.planFilePath,
planTitle: planPayload.planTitle,
planSummary: planPayload.planSummary,
} : {}),
});
}
case 'permission_request':
return createNormalizedMessage({
...base,
kind: 'permission_request',
requestId: message.requestId,
toolName: message.toolName,
input: message.payload,
});
case 'elicitation_request':
return createNormalizedMessage({
...base,
kind: 'interactive_prompt',
requestId: message.requestId,
content: '',
});
case 'structured_output':
return createNormalizedMessage({
...base,
kind: 'status',
text: 'structured',
payload: message.payload,
});
case 'status':
return createNormalizedMessage({ ...base, kind: 'status', text: message.text || '' });
case 'complete':
return createNormalizedMessage({ ...base, kind: 'complete' });
case 'error':
return createNormalizedMessage({ ...base, kind: 'error', content: message.text || '' });
case 'interrupted':
return createNormalizedMessage({ ...base, kind: 'interrupted', content: message.text || '' });
case 'compact_boundary': {
const payload = message.payload || {};
return createNormalizedMessage({
...base,
kind: 'compact_boundary',
trigger: payload.trigger || 'auto',
preTokens: payload.preTokens,
compactLevel: payload.level,
compactStage: payload.stage,
compactStageLabel: payload.stageLabel || payload.stage,
compactMetadata: payload,
});
}
default:
return createNormalizedMessage({ ...base, kind: 'status', text: message.kind });
}
}
export default router;