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:
2026-05-22 06:14:49 +02:00
parent c7a798fe02
commit 930225edb9
11 changed files with 523 additions and 168 deletions

View File

@@ -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);
}