| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- /**
- * 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 } : {}),
- // Inline tool-result images (e.g. read_file on a PNG). The web
- // server already wraps the bare base64 from canonical messages as
- // data URLs in `toWebMessageImage`, so just pass them through.
- ...(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;
|