Fix: real Hermes session integration, artifact detection, SSE robustness
- Parse plain-text 'hermes sessions list' output for real session IDs - Pass sessionId to chat API for --resume continuity across page loads - Filter session_id: lines from SSE content into separate metadata events - Fix crash: 'Controller is already closed' with proper error handling - Auto-detect code blocks, HTML, SVG, diff artifacts from responses - Add diff rendering support in Monaco editor - Fix status/models API for 'hermes config show' output format - Load real Hermes sessions in sidebar on mount - Link local sessions to Hermes session IDs for resumability
This commit is contained in:
@@ -3,21 +3,84 @@ import { execSync } from 'child_process';
|
||||
|
||||
export async function GET({ url }) {
|
||||
try {
|
||||
const limit = url.searchParams.get('limit') || '20';
|
||||
const output = execSync(
|
||||
'hermes sessions list --limit ' + limit + ' --json 2>/dev/null || echo "[]"',
|
||||
{ timeout: 5000, encoding: 'utf-8' }
|
||||
);
|
||||
const parsed = JSON.parse(output);
|
||||
return json(
|
||||
parsed.map((s: any) => ({
|
||||
id: s.id || s.session_id || '',
|
||||
title: s.title || 'Untitled',
|
||||
preview: s.preview || (s.first_message || '').slice(0, 80),
|
||||
timestamp: s.timestamp || s.created_at || Date.now()
|
||||
}))
|
||||
);
|
||||
const limit = url.searchParams.get('limit') || '50';
|
||||
const output = execSync(`hermes sessions list --limit ${limit} 2>/dev/null`, {
|
||||
timeout: 5000,
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
|
||||
const sessions = parseSessions(output);
|
||||
return json(sessions);
|
||||
} catch {
|
||||
return json([]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the plain-text output of `hermes sessions list`.
|
||||
*
|
||||
* Expected format (column-aligned with spaces):
|
||||
* Title Preview Last Active ID
|
||||
* ──────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
* — create a way to connect to hermes via 2m ago 20260522_045714_48480d
|
||||
* Self-Improvement and Token Eff find me good self improvement skills. 45m ago 20260522_045210_2381af
|
||||
*
|
||||
* The ID column is always the last column and matches: YYYYMMDD_HHMMSS_[hex]
|
||||
* The "Last Active" column is second-to-last.
|
||||
*/
|
||||
function parseSessions(output: string) {
|
||||
const lines = output.split('\n').filter((l) => l.trim());
|
||||
const sessions: { id: string; title: string; preview: string; timestamp: number }[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
// Skip header and separator lines
|
||||
if (line.startsWith('Title') || line.startsWith('──')) continue;
|
||||
|
||||
// Match the session ID at the end: YYYYMMDD_HHMMSS_[hex]
|
||||
const idMatch = line.match(/(\d{8}_\d{6}_[0-9a-f]+)\s*$/);
|
||||
if (!idMatch) continue;
|
||||
|
||||
const id = idMatch[1];
|
||||
const beforeId = line.slice(0, line.lastIndexOf(id)).trimEnd();
|
||||
|
||||
// Match the "Last Active" column (relative time like "2m ago", "1h ago", "3d ago")
|
||||
const timeMatch = beforeId.match(/(\d+[smhdw]\s+ago)\s*$/);
|
||||
let timestamp = Date.now();
|
||||
let beforeTime = beforeId;
|
||||
if (timeMatch) {
|
||||
const timeStr = timeMatch[1];
|
||||
beforeTime = beforeId.slice(0, beforeId.lastIndexOf(timeStr)).trimEnd();
|
||||
timestamp = parseRelativeTime(timeStr);
|
||||
}
|
||||
|
||||
// Everything before the time column is Title + Preview
|
||||
// Title is typically up to ~32 chars, Preview fills the rest
|
||||
// We'll split on the first long gap of spaces
|
||||
const parts = beforeTime.split(/\s{3,}/);
|
||||
let title = parts[0] || 'Untitled';
|
||||
let preview = parts.slice(1).join(' ') || '';
|
||||
|
||||
// Clean up title (replace dash-only titles)
|
||||
if (/^[—\-]+$/.test(title)) title = 'Untitled';
|
||||
|
||||
sessions.push({ id, title, preview, timestamp });
|
||||
}
|
||||
|
||||
return sessions;
|
||||
}
|
||||
|
||||
function parseRelativeTime(str: string): number {
|
||||
const now = Date.now();
|
||||
const match = str.match(/^(\d+)\s*([smhdw])\s+ago$/);
|
||||
if (!match) return now;
|
||||
const value = parseInt(match[1]);
|
||||
const unit = match[2];
|
||||
const multipliers: Record<string, number> = {
|
||||
s: 1000,
|
||||
m: 60 * 1000,
|
||||
h: 60 * 60 * 1000,
|
||||
d: 24 * 60 * 60 * 1000,
|
||||
w: 7 * 24 * 60 * 60 * 1000
|
||||
};
|
||||
return now - value * (multipliers[unit] || 0);
|
||||
}
|
||||
Reference in New Issue
Block a user