mirror of
https://github.com/R0m1k3/EveFlow.git
synced 2026-10-12 01:39:13 +02:00
Serveur MCP local - Endpoint /mcp (JSON-RPC 2.0, Streamable HTTP, réponses JSON) sur le serveur webhook : initialize, tools/list, tools/call, ping. Même secret que le webhook. 14 outils : capture_screen (image MCP), lock_session, open_app, open_url, media_key, clipboard_get/set, find_files, speak_text, notify_user, set_hud_state, get_app_status, get_conversation_history, show_message. Les outils UI transitent par IPC vers le renderer. - Les mêmes actions système sont proposées au modèle en chat completions. Notifications - Heures calmes (plage horaire, franchissement de minuit), mots prioritaires, échecs de crons toujours lus, résumé vocal des messages entrants (n premières phrases), thème nuit automatique, compteur de non-lus. Conversation - Mode mission : bouton dans la barre de commande, second modèle Hermes (hermes.missionModel) pour les tâches longues. - Barge-in : la parole de l'utilisateur coupe la voix ; seuil d'énergie et ratio relevés pendant la synthèse pour ignorer l'écho. Widget compact - Bandeau glanceable : état, dernière phrase, non-lus, indicateurs heures calmes et mission. Tests : 28 tests unitaires (heures calmes, priorité, résumé), e2e Electron sous Xvfb : MCP initialize/tools/list/tools/call (presse-papiers, capture en image, état et message via le renderer), bandeau compact, aucune erreur. Claude-Session: https://claude.ai/code/session_017Wn5VX9HNbJ7N54hR24u9Y Co-authored-by: Claude <noreply@anthropic.com>
169 lines
5.8 KiB
TypeScript
169 lines
5.8 KiB
TypeScript
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;
|
|
/** Incremented to make the core flash briefly. */
|
|
pingCount: number;
|
|
/** Time to first token of the current reply, in ms. */
|
|
latencyMs: number | null;
|
|
/** Pushes received while the window was hidden / compact, cleared when the user looks. */
|
|
unread: number;
|
|
/** Mission mode routes the next messages to the "mission" model (long tasks). */
|
|
missionMode: boolean;
|
|
/** True while quiet hours apply (computed by App). */
|
|
quiet: boolean;
|
|
incUnread: () => void;
|
|
markRead: () => void;
|
|
setMissionMode: (on: boolean) => void;
|
|
setQuiet: (quiet: boolean) => void;
|
|
|
|
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;
|
|
ping: () => void;
|
|
setLatency: (ms: number | null) => 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: '',
|
|
pingCount: 0,
|
|
latencyMs: null,
|
|
unread: 0,
|
|
missionMode: false,
|
|
quiet: false,
|
|
incUnread: () => set((s) => ({ unread: Math.min(99, s.unread + 1) })),
|
|
markRead: () => set((s) => (s.unread ? { unread: 0 } : {})),
|
|
setMissionMode: (missionMode) => set({ missionMode }),
|
|
setQuiet: (quiet) => set((s) => (s.quiet === quiet ? {} : { quiet })),
|
|
|
|
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 }),
|
|
ping: () => set((s) => ({ pingCount: s.pingCount + 1 })),
|
|
setLatency: (latencyMs) => set({ latencyMs })
|
|
}));
|