feat!: rebuild EveFlow as a JARVIS-style voice HUD for Hermes Agent (v2.0.0)

Complete rewrite of the application:

- Toolchain: Electron 44, Vite 8, React 19, TypeScript 5.9, Zustand, Vitest;
  main process rewritten in TypeScript (electron/), typed IPC contract (shared/),
  sandboxed renderer with webSecurity on and an HTTP/SSE proxy in the main process.
- Voice: AudioWorklet microphone capture at 16 kHz with adaptive energy VAD and
  auto-stop, WAV encoder, OpenAI-compatible STT, TTS queue with sentence-level
  streaming, prefetch and Web Audio playback feeding an analyser; hands-free mode,
  global push-to-talk hotkey, system-voice fallback.
- Hermes: full API client (capabilities, health, models, skills, toolsets,
  sessions, jobs) with automatic transport selection: runs API (SSE lifecycle,
  approvals, steer, stop) > sessions stream > chat completions with local tools;
  tolerant event normalisation; webhook receiver hardened (secret, size limit).
- UI: JARVIS arc-reactor core on Canvas 2D reacting to the real audio signal,
  holographic HUD layout (transcript, core, Hermes ops panel with tools/crons/
  skills/sessions/link tabs, telemetry), settings drawer, approval modals,
  compact floating widget, four themes, tray icon and global shortcuts.
- Removed the 3D Eve robot, three.js and legacy assets; migrated 1.x settings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Wn5VX9HNbJ7N54hR24u9Y
This commit is contained in:
Claude committed 2026-09-03 13:40:59 +00:00
1 parent 5217eee53a
commit 127a2083a1
239 files changed
+11351 -86495

No files matched your search

+141
View File
@@ -0,0 +1,141 @@
import { create } from 'zustand';
import { uid } from '../lib/id';
import type { HermesUsage } from '../services/hermes/types';
export type HudState = 'idle' | 'listening' | 'thinking' | 'speaking' | 'alert' | 'error' | 'success';
export interface ChatMessage {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
reasoning?: string;
images?: string[];
source?: string; // eveflow | telegram | cron | webhook ...
timestamp: number;
status?: 'streaming' | 'done' | 'error' | 'cancelled';
usage?: HermesUsage;
transport?: string;
jobName?: string;
}
export interface Activity {
id: string;
kind: 'tool' | 'subagent' | 'system' | 'job';
name: string;
status: 'running' | 'done' | 'error';
detail?: string;
output?: string;
startedAt: number;
endedAt?: number;
}
export interface PendingRequest {
id: string; // requestId or synthetic
requestId: string;
runId: string;
kind: 'approval' | 'clarify' | 'input';
title: string;
description: string;
options?: string[];
tool?: string;
args?: string;
secret?: boolean;
createdAt: number;
}
interface ChatStore {
messages: ChatMessage[];
activity: Activity[];
pending: PendingRequest[];
isSending: boolean;
currentRunId: string | null;
hud: HudState;
hudOverride: HudState | null;
error: string | null;
draft: string;
addMessage: (message: Omit<ChatMessage, 'id' | 'timestamp'> & Partial<Pick<ChatMessage, 'id' | 'timestamp'>>) => string;
updateMessage: (id: string, patch: Partial<ChatMessage> | ((m: ChatMessage) => Partial<ChatMessage>)) => void;
appendToMessage: (id: string, delta: string, field?: 'content' | 'reasoning') => void;
removeMessage: (id: string) => void;
clear: () => void;
pushActivity: (activity: Omit<Activity, 'id' | 'startedAt'> & Partial<Pick<Activity, 'id' | 'startedAt'>>) => string;
finishActivity: (match: { id?: string; name?: string }, patch: Partial<Activity>) => void;
addPending: (request: Omit<PendingRequest, 'id' | 'createdAt'>) => void;
removePending: (id: string) => void;
setSending: (sending: boolean, runId?: string | null) => void;
setHud: (state: HudState) => void;
setHudOverride: (state: HudState | null) => void;
setError: (error: string | null) => void;
setDraft: (draft: string) => void;
}
const MAX_MESSAGES = 400;
const MAX_ACTIVITY = 120;
export const useChat = create<ChatStore>((set, get) => ({
messages: [],
activity: [],
pending: [],
isSending: false,
currentRunId: null,
hud: 'idle',
hudOverride: null,
error: null,
draft: '',
addMessage: (message) => {
const id = message.id ?? uid('msg');
const entry: ChatMessage = { ...message, id, timestamp: message.timestamp ?? Date.now() };
set((s) => ({ messages: [...s.messages.slice(-MAX_MESSAGES), entry] }));
return id;
},
updateMessage: (id, patch) =>
set((s) => ({
messages: s.messages.map((m) => (m.id === id ? { ...m, ...(typeof patch === 'function' ? patch(m) : patch) } : m))
})),
appendToMessage: (id, delta, field = 'content') =>
set((s) => ({
messages: s.messages.map((m) => (m.id === id ? { ...m, [field]: (m[field] ?? '') + delta } : m))
})),
removeMessage: (id) => set((s) => ({ messages: s.messages.filter((m) => m.id !== id) })),
clear: () => set({ messages: [], activity: [], pending: [], error: null }),
pushActivity: (activity) => {
const id = activity.id ?? uid('act');
const entry: Activity = { ...activity, id, startedAt: activity.startedAt ?? Date.now() };
set((s) => ({ activity: [...s.activity.slice(-MAX_ACTIVITY), entry] }));
return id;
},
finishActivity: (match, patch) =>
set((s) => {
const list = [...s.activity];
let index = -1;
if (match.id) index = list.findIndex((a) => a.id === match.id);
if (index === -1 && match.name) {
for (let i = list.length - 1; i >= 0; i--) {
if (list[i].name === match.name && list[i].status === 'running') {
index = i;
break;
}
}
}
if (index === -1) return {};
list[index] = { ...list[index], endedAt: Date.now(), ...patch };
return { activity: list };
}),
addPending: (request) =>
set((s) => ({
pending: [...s.pending.filter((p) => p.requestId !== request.requestId || !request.requestId), { ...request, id: uid('req'), createdAt: Date.now() }]
})),
removePending: (id) => set((s) => ({ pending: s.pending.filter((p) => p.id !== id && p.requestId !== id) })),
setSending: (isSending, runId = null) => set({ isSending, currentRunId: isSending ? runId : null }),
setHud: (hud) => {
if (get().hud !== hud) set({ hud });
},
setHudOverride: (hudOverride) => set({ hudOverride }),
setError: (error) => set({ error }),
setDraft: (draft) => set({ draft })
}));