Replace vanilla JS (~1600 lines) with Preact components and consolidate 8 tabs into 4 (Plan, Work, Tools, Dev) for better usability. - Plan tab: plan status/phases + orch canvas (when enabled) - Work tab: git/worktrees + improved session display with human-readable labels - Tools tab: skills + commands + logs + research in card sections - Dev tab: dev preview (unchanged behavior) Add Preact dependency, TSX build via bun, shared hooks (useSSE, useAPI), and utility modules. Simplify index.html to a single mount point. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
53 lines
1.5 KiB
TypeScript
53 lines
1.5 KiB
TypeScript
export function escapeHtml(s: string | null | undefined): string {
|
|
if (!s) return '';
|
|
return s
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
export function escapeAttr(s: string | null | undefined): string {
|
|
if (!s) return '';
|
|
return s
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
export function formatAge(sec: number): string {
|
|
if (sec < 60) return sec + 's ago';
|
|
if (sec < 3600) return Math.floor(sec / 60) + 'm ago';
|
|
return Math.floor(sec / 3600) + 'h ago';
|
|
}
|
|
|
|
export function formatTokens(n: number): string {
|
|
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
return String(n);
|
|
}
|
|
|
|
export function flashSent(el: HTMLElement) {
|
|
el.classList.add('sent');
|
|
setTimeout(() => el.classList.remove('sent'), 600);
|
|
}
|
|
|
|
export function formatSessionLabel(key: string): string {
|
|
if (key.startsWith('heartbeat:')) return 'Heartbeat';
|
|
const parts = key.split(':');
|
|
if (parts.length >= 4) {
|
|
const channel = parts[2]; // "telegram"
|
|
const scope = parts[3]; // "group" or "dm"
|
|
return capitalize(channel) + ' ' + capitalize(scope);
|
|
}
|
|
if (parts.length > 2) return parts.slice(2).join(':');
|
|
return key;
|
|
}
|
|
|
|
function capitalize(s: string): string {
|
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
}
|
|
|
|
export function isFresh(lastUpdate: Record<string, number>, key: string, ms = 5000): boolean {
|
|
return !!lastUpdate[key] && Date.now() - lastUpdate[key] < ms;
|
|
}
|