committed by
GitHub
Unverified
parent
3d804a9f5e
commit
2c5c82bb74
@@ -3,8 +3,9 @@
|
||||
* Manages messaging channel state
|
||||
*/
|
||||
import { create } from 'zustand';
|
||||
import { hostApiFetch } from '@/lib/host-api';
|
||||
import { useGatewayStore } from './gateway';
|
||||
import type { Channel, ChannelType } from '../types/channel';
|
||||
import { invokeIpc } from '@/lib/api-client';
|
||||
|
||||
interface AddChannelParams {
|
||||
type: ChannelType;
|
||||
@@ -18,7 +19,7 @@ interface ChannelsState {
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
fetchChannels: (options?: { probe?: boolean; silent?: boolean }) => Promise<void>;
|
||||
fetchChannels: () => Promise<void>;
|
||||
addChannel: (params: AddChannelParams) => Promise<Channel>;
|
||||
deleteChannel: (channelId: string) => Promise<void>;
|
||||
connectChannel: (channelId: string) => Promise<void>;
|
||||
@@ -34,20 +35,10 @@ export const useChannelsStore = create<ChannelsState>((set, get) => ({
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
fetchChannels: async (options) => {
|
||||
const probe = options?.probe ?? false;
|
||||
const silent = options?.silent ?? false;
|
||||
if (!silent) {
|
||||
set({ loading: true, error: null });
|
||||
}
|
||||
fetchChannels: async () => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const result = await invokeIpc(
|
||||
'gateway:rpc',
|
||||
'channels.status',
|
||||
{ probe }
|
||||
) as {
|
||||
success: boolean;
|
||||
result?: {
|
||||
const data = await useGatewayStore.getState().rpc<{
|
||||
channelOrder?: string[];
|
||||
channels?: Record<string, unknown>;
|
||||
channelAccounts?: Record<string, Array<{
|
||||
@@ -63,12 +54,8 @@ export const useChannelsStore = create<ChannelsState>((set, get) => ({
|
||||
lastOutboundAt?: number | null;
|
||||
}>>;
|
||||
channelDefaultAccountId?: Record<string, string>;
|
||||
};
|
||||
error?: string;
|
||||
};
|
||||
|
||||
if (result.success && result.result) {
|
||||
const data = result.result;
|
||||
}>('channels.status', { probe: true });
|
||||
if (data) {
|
||||
const channels: Channel[] = [];
|
||||
|
||||
// Parse the complex channels.status response into simple Channel objects
|
||||
@@ -131,30 +118,26 @@ export const useChannelsStore = create<ChannelsState>((set, get) => ({
|
||||
});
|
||||
}
|
||||
|
||||
set((state) => ({ channels, loading: silent ? state.loading : false }));
|
||||
set({ channels, loading: false });
|
||||
} else {
|
||||
// Gateway not available - try to show channels from local config
|
||||
set((state) => ({ channels: [], loading: silent ? state.loading : false }));
|
||||
set({ channels: [], loading: false });
|
||||
}
|
||||
} catch {
|
||||
// Gateway not connected, show empty
|
||||
set((state) => ({ channels: [], loading: silent ? state.loading : false }));
|
||||
set({ channels: [], loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
addChannel: async (params) => {
|
||||
try {
|
||||
const result = await invokeIpc(
|
||||
'gateway:rpc',
|
||||
'channels.add',
|
||||
params
|
||||
) as { success: boolean; result?: Channel; error?: string };
|
||||
const result = await useGatewayStore.getState().rpc<Channel>('channels.add', params);
|
||||
|
||||
if (result.success && result.result) {
|
||||
if (result) {
|
||||
set((state) => ({
|
||||
channels: [...state.channels, result.result!],
|
||||
channels: [...state.channels, result],
|
||||
}));
|
||||
return result.result;
|
||||
return result;
|
||||
} else {
|
||||
// If gateway is not available, create a local channel for now
|
||||
const newChannel: Channel = {
|
||||
@@ -189,17 +172,15 @@ export const useChannelsStore = create<ChannelsState>((set, get) => ({
|
||||
|
||||
try {
|
||||
// Delete the channel configuration from openclaw.json
|
||||
await invokeIpc('channel:deleteConfig', channelType);
|
||||
await hostApiFetch(`/api/channels/config/${encodeURIComponent(channelType)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to delete channel config:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
await invokeIpc(
|
||||
'gateway:rpc',
|
||||
'channels.delete',
|
||||
{ channelId: channelType }
|
||||
);
|
||||
await useGatewayStore.getState().rpc('channels.delete', { channelId: channelType });
|
||||
} catch (error) {
|
||||
// Continue with local deletion even if gateway fails
|
||||
console.error('Failed to delete channel from gateway:', error);
|
||||
@@ -216,17 +197,8 @@ export const useChannelsStore = create<ChannelsState>((set, get) => ({
|
||||
updateChannel(channelId, { status: 'connecting', error: undefined });
|
||||
|
||||
try {
|
||||
const result = await invokeIpc(
|
||||
'gateway:rpc',
|
||||
'channels.connect',
|
||||
{ channelId }
|
||||
) as { success: boolean; error?: string };
|
||||
|
||||
if (result.success) {
|
||||
updateChannel(channelId, { status: 'connected' });
|
||||
} else {
|
||||
updateChannel(channelId, { status: 'error', error: result.error });
|
||||
}
|
||||
await useGatewayStore.getState().rpc('channels.connect', { channelId });
|
||||
updateChannel(channelId, { status: 'connected' });
|
||||
} catch (error) {
|
||||
updateChannel(channelId, { status: 'error', error: String(error) });
|
||||
}
|
||||
@@ -236,11 +208,7 @@ export const useChannelsStore = create<ChannelsState>((set, get) => ({
|
||||
const { updateChannel } = get();
|
||||
|
||||
try {
|
||||
await invokeIpc(
|
||||
'gateway:rpc',
|
||||
'channels.disconnect',
|
||||
{ channelId }
|
||||
);
|
||||
await useGatewayStore.getState().rpc('channels.disconnect', { channelId });
|
||||
} catch (error) {
|
||||
console.error('Failed to disconnect channel:', error);
|
||||
}
|
||||
@@ -249,17 +217,10 @@ export const useChannelsStore = create<ChannelsState>((set, get) => ({
|
||||
},
|
||||
|
||||
requestQrCode: async (channelType) => {
|
||||
const result = await invokeIpc(
|
||||
'gateway:rpc',
|
||||
return await useGatewayStore.getState().rpc<{ qrCode: string; sessionId: string }>(
|
||||
'channels.requestQr',
|
||||
{ type: channelType }
|
||||
) as { success: boolean; result?: { qrCode: string; sessionId: string }; error?: string };
|
||||
|
||||
if (result.success && result.result) {
|
||||
return result.result;
|
||||
}
|
||||
|
||||
throw new Error(result.error || 'Failed to request QR code');
|
||||
{ type: channelType },
|
||||
);
|
||||
},
|
||||
|
||||
setChannels: (channels) => set({ channels }),
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* Chat State Store
|
||||
* Manages chat messages, sessions, streaming, and thinking state.
|
||||
* Communicates with OpenClaw Gateway via gateway:rpc IPC.
|
||||
* Communicates with OpenClaw Gateway via renderer WebSocket RPC.
|
||||
*/
|
||||
import { create } from 'zustand';
|
||||
import { invokeIpc } from '@/lib/api-client';
|
||||
import { hostApiFetch } from '@/lib/host-api';
|
||||
import { useGatewayStore } from './gateway';
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -597,10 +598,13 @@ async function loadMissingPreviews(messages: RawMessage[]): Promise<boolean> {
|
||||
if (needPreview.length === 0) return false;
|
||||
|
||||
try {
|
||||
const thumbnails = await invokeIpc(
|
||||
'media:getThumbnails',
|
||||
needPreview,
|
||||
) as Record<string, { preview: string | null; fileSize: number }>;
|
||||
const thumbnails = await hostApiFetch<Record<string, { preview: string | null; fileSize: number }>>(
|
||||
'/api/files/thumbnails',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ paths: needPreview }),
|
||||
},
|
||||
);
|
||||
|
||||
let updated = false;
|
||||
for (const msg of messages) {
|
||||
@@ -929,14 +933,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
|
||||
loadSessions: async () => {
|
||||
try {
|
||||
const result = await invokeIpc(
|
||||
'gateway:rpc',
|
||||
'sessions.list',
|
||||
{}
|
||||
) as { success: boolean; result?: Record<string, unknown>; error?: string };
|
||||
|
||||
if (result.success && result.result) {
|
||||
const data = result.result;
|
||||
const data = await useGatewayStore.getState().rpc<Record<string, unknown>>('sessions.list', {});
|
||||
if (data) {
|
||||
const rawSessions = Array.isArray(data.sessions) ? data.sessions : [];
|
||||
const sessions: ChatSession[] = rawSessions.map((s: Record<string, unknown>) => ({
|
||||
key: String(s.key || ''),
|
||||
@@ -1002,13 +1000,11 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
void Promise.all(
|
||||
sessionsToLabel.map(async (session) => {
|
||||
try {
|
||||
const r = await invokeIpc(
|
||||
'gateway:rpc',
|
||||
const r = await useGatewayStore.getState().rpc<Record<string, unknown>>(
|
||||
'chat.history',
|
||||
{ sessionKey: session.key, limit: 1000 },
|
||||
) as { success: boolean; result?: Record<string, unknown> };
|
||||
if (!r.success || !r.result) return;
|
||||
const msgs = Array.isArray(r.result.messages) ? r.result.messages as RawMessage[] : [];
|
||||
);
|
||||
const msgs = Array.isArray(r.messages) ? r.messages as RawMessage[] : [];
|
||||
const firstUser = msgs.find((m) => m.role === 'user');
|
||||
const lastMsg = msgs[msgs.length - 1];
|
||||
set((s) => {
|
||||
@@ -1078,10 +1074,13 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
// The main process renames <suffix>.jsonl → <suffix>.deleted.jsonl so that
|
||||
// sessions.list and token-usage queries both skip it automatically.
|
||||
try {
|
||||
const result = await invokeIpc('session:delete', key) as {
|
||||
const result = await hostApiFetch<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
};
|
||||
}>('/api/sessions/delete', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ sessionKey: key }),
|
||||
});
|
||||
if (!result.success) {
|
||||
console.warn(`[deleteSession] IPC reported failure for ${key}:`, result.error);
|
||||
}
|
||||
@@ -1186,14 +1185,11 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
if (!quiet) set({ loading: true, error: null });
|
||||
|
||||
try {
|
||||
const result = await invokeIpc(
|
||||
'gateway:rpc',
|
||||
const data = await useGatewayStore.getState().rpc<Record<string, unknown>>(
|
||||
'chat.history',
|
||||
{ sessionKey: currentSessionKey, limit: 200 }
|
||||
) as { success: boolean; result?: Record<string, unknown>; error?: string };
|
||||
|
||||
if (result.success && result.result) {
|
||||
const data = result.result;
|
||||
{ sessionKey: currentSessionKey, limit: 200 },
|
||||
);
|
||||
if (data) {
|
||||
const rawMessages = Array.isArray(data.messages) ? data.messages as RawMessage[] : [];
|
||||
|
||||
// Before filtering: attach images/files from tool_result messages to the next assistant message
|
||||
@@ -1426,23 +1422,25 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
const CHAT_SEND_TIMEOUT_MS = 120_000;
|
||||
|
||||
if (hasMedia) {
|
||||
result = await invokeIpc(
|
||||
'chat:sendWithMedia',
|
||||
result = await hostApiFetch<{ success: boolean; result?: { runId?: string }; error?: string }>(
|
||||
'/api/chat/send-with-media',
|
||||
{
|
||||
sessionKey: currentSessionKey,
|
||||
message: trimmed || 'Process the attached file(s).',
|
||||
deliver: false,
|
||||
idempotencyKey,
|
||||
media: attachments.map((a) => ({
|
||||
filePath: a.stagedPath,
|
||||
mimeType: a.mimeType,
|
||||
fileName: a.fileName,
|
||||
})),
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
sessionKey: currentSessionKey,
|
||||
message: trimmed || 'Process the attached file(s).',
|
||||
deliver: false,
|
||||
idempotencyKey,
|
||||
media: attachments.map((a) => ({
|
||||
filePath: a.stagedPath,
|
||||
mimeType: a.mimeType,
|
||||
fileName: a.fileName,
|
||||
})),
|
||||
}),
|
||||
},
|
||||
) as { success: boolean; result?: { runId?: string }; error?: string };
|
||||
);
|
||||
} else {
|
||||
result = await invokeIpc(
|
||||
'gateway:rpc',
|
||||
const rpcResult = await useGatewayStore.getState().rpc<{ runId?: string }>(
|
||||
'chat.send',
|
||||
{
|
||||
sessionKey: currentSessionKey,
|
||||
@@ -1451,7 +1449,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
idempotencyKey,
|
||||
},
|
||||
CHAT_SEND_TIMEOUT_MS,
|
||||
) as { success: boolean; result?: { runId?: string }; error?: string };
|
||||
);
|
||||
result = { success: true, result: rpcResult };
|
||||
}
|
||||
|
||||
console.log(`[sendMessage] RPC result: success=${result.success}, runId=${result.result?.runId || 'none'}`);
|
||||
@@ -1478,8 +1477,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
set({ streamingTools: [] });
|
||||
|
||||
try {
|
||||
await invokeIpc(
|
||||
'gateway:rpc',
|
||||
await useGatewayStore.getState().rpc(
|
||||
'chat.abort',
|
||||
{ sessionKey: currentSessionKey },
|
||||
);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* Manages scheduled task state
|
||||
*/
|
||||
import { create } from 'zustand';
|
||||
import { hostApiFetch } from '@/lib/host-api';
|
||||
import type { CronJob, CronJobCreateInput, CronJobUpdateInput } from '../types/cron';
|
||||
import { invokeIpc } from '@/lib/api-client';
|
||||
|
||||
interface CronState {
|
||||
jobs: CronJob[];
|
||||
@@ -30,7 +30,7 @@ export const useCronStore = create<CronState>((set) => ({
|
||||
set({ loading: true, error: null });
|
||||
|
||||
try {
|
||||
const result = await invokeIpc<CronJob[]>('cron:list');
|
||||
const result = await hostApiFetch<CronJob[]>('/api/cron/jobs');
|
||||
set({ jobs: result, loading: false });
|
||||
} catch (error) {
|
||||
set({ error: String(error), loading: false });
|
||||
@@ -39,7 +39,10 @@ export const useCronStore = create<CronState>((set) => ({
|
||||
|
||||
createJob: async (input) => {
|
||||
try {
|
||||
const job = await invokeIpc<CronJob>('cron:create', input);
|
||||
const job = await hostApiFetch<CronJob>('/api/cron/jobs', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
set((state) => ({ jobs: [...state.jobs, job] }));
|
||||
return job;
|
||||
} catch (error) {
|
||||
@@ -50,7 +53,10 @@ export const useCronStore = create<CronState>((set) => ({
|
||||
|
||||
updateJob: async (id, input) => {
|
||||
try {
|
||||
await invokeIpc('cron:update', id, input);
|
||||
await hostApiFetch(`/api/cron/jobs/${encodeURIComponent(id)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
set((state) => ({
|
||||
jobs: state.jobs.map((job) =>
|
||||
job.id === id ? { ...job, ...input, updatedAt: new Date().toISOString() } : job
|
||||
@@ -64,7 +70,9 @@ export const useCronStore = create<CronState>((set) => ({
|
||||
|
||||
deleteJob: async (id) => {
|
||||
try {
|
||||
await invokeIpc('cron:delete', id);
|
||||
await hostApiFetch(`/api/cron/jobs/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
set((state) => ({
|
||||
jobs: state.jobs.filter((job) => job.id !== id),
|
||||
}));
|
||||
@@ -76,7 +84,10 @@ export const useCronStore = create<CronState>((set) => ({
|
||||
|
||||
toggleJob: async (id, enabled) => {
|
||||
try {
|
||||
await invokeIpc('cron:toggle', id, enabled);
|
||||
await hostApiFetch('/api/cron/toggle', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ id, enabled }),
|
||||
});
|
||||
set((state) => ({
|
||||
jobs: state.jobs.map((job) =>
|
||||
job.id === id ? { ...job, enabled } : job
|
||||
@@ -90,11 +101,14 @@ export const useCronStore = create<CronState>((set) => ({
|
||||
|
||||
triggerJob: async (id) => {
|
||||
try {
|
||||
const result = await invokeIpc<unknown>('cron:trigger', id);
|
||||
const result = await hostApiFetch('/api/cron/trigger', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ id }),
|
||||
});
|
||||
console.log('Cron trigger result:', result);
|
||||
// Refresh jobs after trigger to update lastRun/nextRun state
|
||||
try {
|
||||
const jobs = await invokeIpc<CronJob[]>('cron:list');
|
||||
const jobs = await hostApiFetch<CronJob[]>('/api/cron/jobs');
|
||||
set({ jobs });
|
||||
} catch {
|
||||
// Ignore refresh error
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
/**
|
||||
* Gateway State Store
|
||||
* Manages Gateway connection state and communication
|
||||
* Uses Host API + SSE for lifecycle/status and a direct renderer WebSocket for runtime RPC.
|
||||
*/
|
||||
import { create } from 'zustand';
|
||||
import type { GatewayStatus } from '../types/gateway';
|
||||
import { createHostEventSource, hostApiFetch } from '@/lib/host-api';
|
||||
import { invokeIpc } from '@/lib/api-client';
|
||||
import type { GatewayStatus } from '../types/gateway';
|
||||
|
||||
let gatewayInitPromise: Promise<void> | null = null;
|
||||
let gatewayEventSource: EventSource | null = null;
|
||||
|
||||
interface GatewayHealth {
|
||||
ok: boolean;
|
||||
@@ -19,8 +21,6 @@ interface GatewayState {
|
||||
health: GatewayHealth | null;
|
||||
isInitialized: boolean;
|
||||
lastError: string | null;
|
||||
|
||||
// Actions
|
||||
init: () => Promise<void>;
|
||||
start: () => Promise<void>;
|
||||
stop: () => Promise<void>;
|
||||
@@ -31,6 +31,102 @@ interface GatewayState {
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
function handleGatewayNotification(notification: { method?: string; params?: Record<string, unknown> } | undefined): void {
|
||||
const payload = notification;
|
||||
if (!payload || payload.method !== 'agent' || !payload.params || typeof payload.params !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
const p = payload.params;
|
||||
const data = (p.data && typeof p.data === 'object') ? (p.data as Record<string, unknown>) : {};
|
||||
const phase = data.phase ?? p.phase;
|
||||
const hasChatData = (p.state ?? data.state) || (p.message ?? data.message);
|
||||
|
||||
if (hasChatData) {
|
||||
const normalizedEvent: Record<string, unknown> = {
|
||||
...data,
|
||||
runId: p.runId ?? data.runId,
|
||||
sessionKey: p.sessionKey ?? data.sessionKey,
|
||||
stream: p.stream ?? data.stream,
|
||||
seq: p.seq ?? data.seq,
|
||||
state: p.state ?? data.state,
|
||||
message: p.message ?? data.message,
|
||||
};
|
||||
import('./chat')
|
||||
.then(({ useChatStore }) => {
|
||||
useChatStore.getState().handleChatEvent(normalizedEvent);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
const runId = p.runId ?? data.runId;
|
||||
const sessionKey = p.sessionKey ?? data.sessionKey;
|
||||
if (phase === 'started' && runId != null && sessionKey != null) {
|
||||
import('./chat')
|
||||
.then(({ useChatStore }) => {
|
||||
useChatStore.getState().handleChatEvent({
|
||||
state: 'started',
|
||||
runId,
|
||||
sessionKey,
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
if (phase === 'completed' || phase === 'done' || phase === 'finished' || phase === 'end') {
|
||||
import('./chat')
|
||||
.then(({ useChatStore }) => {
|
||||
const state = useChatStore.getState();
|
||||
state.loadHistory(true);
|
||||
if (state.sending) {
|
||||
useChatStore.setState({
|
||||
sending: false,
|
||||
activeRunId: null,
|
||||
pendingFinal: false,
|
||||
lastUserMessageAt: null,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function handleGatewayChatMessage(data: unknown): void {
|
||||
import('./chat').then(({ useChatStore }) => {
|
||||
const chatData = data as Record<string, unknown>;
|
||||
const payload = ('message' in chatData && typeof chatData.message === 'object')
|
||||
? chatData.message as Record<string, unknown>
|
||||
: chatData;
|
||||
|
||||
if (payload.state) {
|
||||
useChatStore.getState().handleChatEvent(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
useChatStore.getState().handleChatEvent({
|
||||
state: 'final',
|
||||
message: payload,
|
||||
runId: chatData.runId ?? payload.runId,
|
||||
});
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function mapChannelStatus(status: string): 'connected' | 'connecting' | 'disconnected' | 'error' {
|
||||
switch (status) {
|
||||
case 'connected':
|
||||
case 'running':
|
||||
return 'connected';
|
||||
case 'connecting':
|
||||
case 'starting':
|
||||
return 'connecting';
|
||||
case 'error':
|
||||
case 'failed':
|
||||
return 'error';
|
||||
default:
|
||||
return 'disconnected';
|
||||
}
|
||||
}
|
||||
|
||||
export const useGatewayStore = create<GatewayState>((set, get) => ({
|
||||
status: {
|
||||
state: 'stopped',
|
||||
@@ -49,141 +145,41 @@ export const useGatewayStore = create<GatewayState>((set, get) => ({
|
||||
|
||||
gatewayInitPromise = (async () => {
|
||||
try {
|
||||
// Get initial status first
|
||||
const status = await invokeIpc('gateway:status') as GatewayStatus;
|
||||
const status = await hostApiFetch<GatewayStatus>('/api/gateway/status');
|
||||
set({ status, isInitialized: true });
|
||||
|
||||
// Listen for status changes
|
||||
window.electron.ipcRenderer.on('gateway:status-changed', (newStatus) => {
|
||||
set({ status: newStatus as GatewayStatus });
|
||||
});
|
||||
|
||||
// Listen for errors
|
||||
window.electron.ipcRenderer.on('gateway:error', (error) => {
|
||||
set({ lastError: String(error) });
|
||||
});
|
||||
|
||||
// Some Gateway builds stream chat events via generic "agent" notifications.
|
||||
// Normalize and forward them to the chat store.
|
||||
// The Gateway may put event fields (state, message, etc.) either inside
|
||||
// params.data or directly on params — we must handle both layouts.
|
||||
window.electron.ipcRenderer.on('gateway:notification', (notification) => {
|
||||
const payload = notification as { method?: string; params?: Record<string, unknown> } | undefined;
|
||||
if (!payload || payload.method !== 'agent' || !payload.params || typeof payload.params !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
const p = payload.params;
|
||||
const data = (p.data && typeof p.data === 'object') ? (p.data as Record<string, unknown>) : {};
|
||||
const phase = data.phase ?? p.phase;
|
||||
|
||||
const hasChatData = (p.state ?? data.state) || (p.message ?? data.message);
|
||||
if (hasChatData) {
|
||||
const normalizedEvent: Record<string, unknown> = {
|
||||
...data,
|
||||
runId: p.runId ?? data.runId,
|
||||
sessionKey: p.sessionKey ?? data.sessionKey,
|
||||
stream: p.stream ?? data.stream,
|
||||
seq: p.seq ?? data.seq,
|
||||
state: p.state ?? data.state,
|
||||
message: p.message ?? data.message,
|
||||
};
|
||||
import('./chat')
|
||||
.then(({ useChatStore }) => {
|
||||
useChatStore.getState().handleChatEvent(normalizedEvent);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// When a run starts (e.g. user clicked Send on console), show loading in the app immediately.
|
||||
const runId = p.runId ?? data.runId;
|
||||
const sessionKey = p.sessionKey ?? data.sessionKey;
|
||||
if (phase === 'started' && runId != null && sessionKey != null) {
|
||||
import('./chat')
|
||||
.then(({ useChatStore }) => {
|
||||
useChatStore.getState().handleChatEvent({
|
||||
state: 'started',
|
||||
runId,
|
||||
sessionKey,
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// When the agent run completes, reload history to get the final response.
|
||||
if (phase === 'completed' || phase === 'done' || phase === 'finished' || phase === 'end') {
|
||||
import('./chat')
|
||||
.then(({ useChatStore }) => {
|
||||
const state = useChatStore.getState();
|
||||
// Always reload history on agent completion, regardless of
|
||||
// the `sending` flag. After a transient error the flag may
|
||||
// already be false, but the Gateway may have retried and
|
||||
// completed successfully in the background.
|
||||
state.loadHistory(true);
|
||||
if (state.sending) {
|
||||
useChatStore.setState({
|
||||
sending: false,
|
||||
activeRunId: null,
|
||||
pendingFinal: false,
|
||||
lastUserMessageAt: null,
|
||||
});
|
||||
if (!gatewayEventSource) {
|
||||
gatewayEventSource = createHostEventSource();
|
||||
gatewayEventSource.addEventListener('gateway:status', (event) => {
|
||||
set({ status: JSON.parse((event as MessageEvent).data) as GatewayStatus });
|
||||
});
|
||||
gatewayEventSource.addEventListener('gateway:error', (event) => {
|
||||
const payload = JSON.parse((event as MessageEvent).data) as { message?: string };
|
||||
set({ lastError: payload.message || 'Gateway error' });
|
||||
});
|
||||
gatewayEventSource.addEventListener('gateway:notification', (event) => {
|
||||
handleGatewayNotification(JSON.parse((event as MessageEvent).data) as {
|
||||
method?: string;
|
||||
params?: Record<string, unknown>;
|
||||
});
|
||||
});
|
||||
gatewayEventSource.addEventListener('gateway:chat-message', (event) => {
|
||||
handleGatewayChatMessage(JSON.parse((event as MessageEvent).data));
|
||||
});
|
||||
gatewayEventSource.addEventListener('gateway:channel-status', (event) => {
|
||||
import('./channels')
|
||||
.then(({ useChannelsStore }) => {
|
||||
const update = JSON.parse((event as MessageEvent).data) as { channelId?: string; status?: string };
|
||||
if (!update.channelId || !update.status) return;
|
||||
const state = useChannelsStore.getState();
|
||||
const channel = state.channels.find((item) => item.type === update.channelId);
|
||||
if (channel) {
|
||||
state.updateChannel(channel.id, { status: mapChannelStatus(update.status) });
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for chat events from the gateway and forward to chat store.
|
||||
// The data arrives as { message: payload } from handleProtocolEvent.
|
||||
// The payload may be a full event wrapper ({ state, runId, message })
|
||||
// or the raw chat message itself. We need to handle both.
|
||||
window.electron.ipcRenderer.on('gateway:chat-message', (data) => {
|
||||
try {
|
||||
import('./chat').then(({ useChatStore }) => {
|
||||
const chatData = data as Record<string, unknown>;
|
||||
const payload = ('message' in chatData && typeof chatData.message === 'object')
|
||||
? chatData.message as Record<string, unknown>
|
||||
: chatData;
|
||||
|
||||
if (payload.state) {
|
||||
useChatStore.getState().handleChatEvent(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
// Raw message without state wrapper — treat as final
|
||||
useChatStore.getState().handleChatEvent({
|
||||
state: 'final',
|
||||
message: payload,
|
||||
runId: chatData.runId ?? payload.runId,
|
||||
});
|
||||
}).catch(() => {});
|
||||
} catch {
|
||||
// Silently ignore forwarding failures
|
||||
}
|
||||
});
|
||||
|
||||
// Catch-all: handle unmatched gateway messages that fell through
|
||||
// all protocol/notification handlers in the main process.
|
||||
// This prevents events from being silently lost.
|
||||
window.electron.ipcRenderer.on('gateway:message', (data) => {
|
||||
if (!data || typeof data !== 'object') return;
|
||||
const msg = data as Record<string, unknown>;
|
||||
|
||||
// Try to detect if this is a chat-related event and forward it
|
||||
if (msg.state && msg.message) {
|
||||
import('./chat').then(({ useChatStore }) => {
|
||||
useChatStore.getState().handleChatEvent(msg);
|
||||
}).catch(() => {});
|
||||
} else if (msg.role && msg.content) {
|
||||
import('./chat').then(({ useChatStore }) => {
|
||||
useChatStore.getState().handleChatEvent({
|
||||
state: 'final',
|
||||
message: msg,
|
||||
});
|
||||
}).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Gateway:', error);
|
||||
set({ lastError: String(error) });
|
||||
@@ -198,25 +194,26 @@ export const useGatewayStore = create<GatewayState>((set, get) => ({
|
||||
start: async () => {
|
||||
try {
|
||||
set({ status: { ...get().status, state: 'starting' }, lastError: null });
|
||||
const result = await invokeIpc('gateway:start') as { success: boolean; error?: string };
|
||||
|
||||
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/gateway/start', {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!result.success) {
|
||||
set({
|
||||
status: { ...get().status, state: 'error', error: result.error },
|
||||
lastError: result.error || 'Failed to start Gateway'
|
||||
lastError: result.error || 'Failed to start Gateway',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
set({
|
||||
status: { ...get().status, state: 'error', error: String(error) },
|
||||
lastError: String(error)
|
||||
lastError: String(error),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
stop: async () => {
|
||||
try {
|
||||
await invokeIpc('gateway:stop');
|
||||
await hostApiFetch('/api/gateway/stop', { method: 'POST' });
|
||||
set({ status: { ...get().status, state: 'stopped' }, lastError: null });
|
||||
} catch (error) {
|
||||
console.error('Failed to stop Gateway:', error);
|
||||
@@ -227,39 +224,28 @@ export const useGatewayStore = create<GatewayState>((set, get) => ({
|
||||
restart: async () => {
|
||||
try {
|
||||
set({ status: { ...get().status, state: 'starting' }, lastError: null });
|
||||
const result = await invokeIpc('gateway:restart') as { success: boolean; error?: string };
|
||||
|
||||
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/gateway/restart', {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!result.success) {
|
||||
set({
|
||||
status: { ...get().status, state: 'error', error: result.error },
|
||||
lastError: result.error || 'Failed to restart Gateway'
|
||||
lastError: result.error || 'Failed to restart Gateway',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
set({
|
||||
status: { ...get().status, state: 'error', error: String(error) },
|
||||
lastError: String(error)
|
||||
lastError: String(error),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
checkHealth: async () => {
|
||||
try {
|
||||
const result = await invokeIpc('gateway:health') as {
|
||||
success: boolean;
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
uptime?: number
|
||||
};
|
||||
|
||||
const health: GatewayHealth = {
|
||||
ok: result.ok,
|
||||
error: result.error,
|
||||
uptime: result.uptime,
|
||||
};
|
||||
|
||||
set({ health });
|
||||
return health;
|
||||
const result = await hostApiFetch<GatewayHealth>('/api/gateway/health');
|
||||
set({ health: result });
|
||||
return result;
|
||||
} catch (error) {
|
||||
const health: GatewayHealth = { ok: false, error: String(error) };
|
||||
set({ health });
|
||||
@@ -268,20 +254,17 @@ export const useGatewayStore = create<GatewayState>((set, get) => ({
|
||||
},
|
||||
|
||||
rpc: async <T>(method: string, params?: unknown, timeoutMs?: number): Promise<T> => {
|
||||
const result = await invokeIpc('gateway:rpc', method, params, timeoutMs) as {
|
||||
const response = await invokeIpc<{
|
||||
success: boolean;
|
||||
result?: T;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || `RPC call failed: ${method}`);
|
||||
}>('gateway:rpc', method, params, timeoutMs);
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || `Gateway RPC failed: ${method}`);
|
||||
}
|
||||
|
||||
return result.result as T;
|
||||
return response.result as T;
|
||||
},
|
||||
|
||||
setStatus: (status) => set({ status }),
|
||||
|
||||
clearError: () => set({ lastError: null }),
|
||||
}));
|
||||
|
||||
@@ -3,23 +3,53 @@
|
||||
* Manages AI provider configurations
|
||||
*/
|
||||
import { create } from 'zustand';
|
||||
import type { ProviderConfig, ProviderWithKeyInfo } from '@/lib/providers';
|
||||
import { invokeIpc } from '@/lib/api-client';
|
||||
import type {
|
||||
ProviderAccount,
|
||||
ProviderConfig,
|
||||
ProviderVendorInfo,
|
||||
ProviderWithKeyInfo,
|
||||
} from '@/lib/providers';
|
||||
import { hostApiFetch } from '@/lib/host-api';
|
||||
import {
|
||||
fetchProviderSnapshot,
|
||||
} from '@/lib/provider-accounts';
|
||||
|
||||
// Re-export types for consumers that imported from here
|
||||
export type { ProviderConfig, ProviderWithKeyInfo } from '@/lib/providers';
|
||||
export type {
|
||||
ProviderAccount,
|
||||
ProviderConfig,
|
||||
ProviderVendorInfo,
|
||||
ProviderWithKeyInfo,
|
||||
} from '@/lib/providers';
|
||||
export type { ProviderSnapshot } from '@/lib/provider-accounts';
|
||||
|
||||
interface ProviderState {
|
||||
providers: ProviderWithKeyInfo[];
|
||||
defaultProviderId: string | null;
|
||||
statuses: ProviderWithKeyInfo[];
|
||||
accounts: ProviderAccount[];
|
||||
vendors: ProviderVendorInfo[];
|
||||
defaultAccountId: string | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
refreshProviderSnapshot: () => Promise<void>;
|
||||
createAccount: (account: ProviderAccount, apiKey?: string) => Promise<void>;
|
||||
removeAccount: (accountId: string) => Promise<void>;
|
||||
validateAccountApiKey: (
|
||||
accountId: string,
|
||||
apiKey: string,
|
||||
options?: { baseUrl?: string }
|
||||
) => Promise<{ valid: boolean; error?: string }>;
|
||||
getAccountApiKey: (accountId: string) => Promise<string | null>;
|
||||
|
||||
// Legacy compatibility aliases
|
||||
fetchProviders: () => Promise<void>;
|
||||
addProvider: (config: Omit<ProviderConfig, 'createdAt' | 'updatedAt'>, apiKey?: string) => Promise<void>;
|
||||
addAccount: (account: ProviderAccount, apiKey?: string) => Promise<void>;
|
||||
updateProvider: (providerId: string, updates: Partial<ProviderConfig>, apiKey?: string) => Promise<void>;
|
||||
updateAccount: (accountId: string, updates: Partial<ProviderAccount>, apiKey?: string) => Promise<void>;
|
||||
deleteProvider: (providerId: string) => Promise<void>;
|
||||
deleteAccount: (accountId: string) => Promise<void>;
|
||||
setApiKey: (providerId: string, apiKey: string) => Promise<void>;
|
||||
updateProviderWithKey: (
|
||||
providerId: string,
|
||||
@@ -28,6 +58,7 @@ interface ProviderState {
|
||||
) => Promise<void>;
|
||||
deleteApiKey: (providerId: string) => Promise<void>;
|
||||
setDefaultProvider: (providerId: string) => Promise<void>;
|
||||
setDefaultAccount: (accountId: string) => Promise<void>;
|
||||
validateApiKey: (
|
||||
providerId: string,
|
||||
apiKey: string,
|
||||
@@ -37,27 +68,32 @@ interface ProviderState {
|
||||
}
|
||||
|
||||
export const useProviderStore = create<ProviderState>((set, get) => ({
|
||||
providers: [],
|
||||
defaultProviderId: null,
|
||||
statuses: [],
|
||||
accounts: [],
|
||||
vendors: [],
|
||||
defaultAccountId: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
fetchProviders: async () => {
|
||||
refreshProviderSnapshot: async () => {
|
||||
set({ loading: true, error: null });
|
||||
|
||||
try {
|
||||
const providers = await invokeIpc<ProviderWithKeyInfo[]>('provider:list');
|
||||
const defaultId = await invokeIpc<string | null>('provider:getDefault');
|
||||
const snapshot = await fetchProviderSnapshot();
|
||||
|
||||
set({
|
||||
providers,
|
||||
defaultProviderId: defaultId,
|
||||
statuses: snapshot.statuses,
|
||||
accounts: snapshot.accounts,
|
||||
vendors: snapshot.vendors,
|
||||
defaultAccountId: snapshot.defaultAccountId,
|
||||
loading: false
|
||||
});
|
||||
} catch (error) {
|
||||
set({ error: String(error), loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
fetchProviders: async () => get().refreshProviderSnapshot(),
|
||||
|
||||
addProvider: async (config, apiKey) => {
|
||||
try {
|
||||
@@ -67,23 +103,46 @@ export const useProviderStore = create<ProviderState>((set, get) => ({
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const result = await invokeIpc<{ success: boolean; error?: string }>('provider:save', fullConfig, apiKey);
|
||||
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/providers', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ config: fullConfig, apiKey }),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to save provider');
|
||||
}
|
||||
|
||||
// Refresh the list
|
||||
await get().fetchProviders();
|
||||
await get().refreshProviderSnapshot();
|
||||
} catch (error) {
|
||||
console.error('Failed to add provider:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
createAccount: async (account, apiKey) => {
|
||||
try {
|
||||
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/provider-accounts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ account, apiKey }),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to create provider account');
|
||||
}
|
||||
|
||||
await get().refreshProviderSnapshot();
|
||||
} catch (error) {
|
||||
console.error('Failed to add account:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
addAccount: async (account, apiKey) => get().createAccount(account, apiKey),
|
||||
|
||||
updateProvider: async (providerId, updates, apiKey) => {
|
||||
try {
|
||||
const existing = get().providers.find((p) => p.id === providerId);
|
||||
const existing = get().statuses.find((p) => p.id === providerId);
|
||||
if (!existing) {
|
||||
throw new Error('Provider not found');
|
||||
}
|
||||
@@ -96,46 +155,91 @@ export const useProviderStore = create<ProviderState>((set, get) => ({
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const result = await invokeIpc<{ success: boolean; error?: string }>('provider:save', updatedConfig, apiKey);
|
||||
const result = await hostApiFetch<{ success: boolean; error?: string }>(`/api/providers/${encodeURIComponent(providerId)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ updates: updatedConfig, apiKey }),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to update provider');
|
||||
}
|
||||
|
||||
// Refresh the list
|
||||
await get().fetchProviders();
|
||||
await get().refreshProviderSnapshot();
|
||||
} catch (error) {
|
||||
console.error('Failed to update provider:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
updateAccount: async (accountId, updates, apiKey) => {
|
||||
try {
|
||||
const result = await hostApiFetch<{ success: boolean; error?: string }>(`/api/provider-accounts/${encodeURIComponent(accountId)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ updates, apiKey }),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to update provider account');
|
||||
}
|
||||
|
||||
await get().refreshProviderSnapshot();
|
||||
} catch (error) {
|
||||
console.error('Failed to update account:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
deleteProvider: async (providerId) => {
|
||||
try {
|
||||
const result = await invokeIpc<{ success: boolean; error?: string }>('provider:delete', providerId);
|
||||
const result = await hostApiFetch<{ success: boolean; error?: string }>(`/api/providers/${encodeURIComponent(providerId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to delete provider');
|
||||
}
|
||||
|
||||
// Refresh the list
|
||||
await get().fetchProviders();
|
||||
await get().refreshProviderSnapshot();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete provider:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
removeAccount: async (accountId) => {
|
||||
try {
|
||||
const result = await hostApiFetch<{ success: boolean; error?: string }>(`/api/provider-accounts/${encodeURIComponent(accountId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to delete provider account');
|
||||
}
|
||||
|
||||
await get().refreshProviderSnapshot();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete account:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
deleteAccount: async (accountId) => get().removeAccount(accountId),
|
||||
|
||||
setApiKey: async (providerId, apiKey) => {
|
||||
try {
|
||||
const result = await invokeIpc<{ success: boolean; error?: string }>('provider:setApiKey', providerId, apiKey);
|
||||
const result = await hostApiFetch<{ success: boolean; error?: string }>(`/api/providers/${encodeURIComponent(providerId)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ updates: {}, apiKey }),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to set API key');
|
||||
}
|
||||
|
||||
// Refresh the list
|
||||
await get().fetchProviders();
|
||||
await get().refreshProviderSnapshot();
|
||||
} catch (error) {
|
||||
console.error('Failed to set API key:', error);
|
||||
throw error;
|
||||
@@ -144,18 +248,16 @@ export const useProviderStore = create<ProviderState>((set, get) => ({
|
||||
|
||||
updateProviderWithKey: async (providerId, updates, apiKey) => {
|
||||
try {
|
||||
const result = await invokeIpc<{ success: boolean; error?: string }>(
|
||||
'provider:updateWithKey',
|
||||
providerId,
|
||||
updates,
|
||||
apiKey
|
||||
);
|
||||
const result = await hostApiFetch<{ success: boolean; error?: string }>(`/api/providers/${encodeURIComponent(providerId)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ updates, apiKey }),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to update provider');
|
||||
}
|
||||
|
||||
await get().fetchProviders();
|
||||
await get().refreshProviderSnapshot();
|
||||
} catch (error) {
|
||||
console.error('Failed to update provider with key:', error);
|
||||
throw error;
|
||||
@@ -164,14 +266,17 @@ export const useProviderStore = create<ProviderState>((set, get) => ({
|
||||
|
||||
deleteApiKey: async (providerId) => {
|
||||
try {
|
||||
const result = await invokeIpc<{ success: boolean; error?: string }>('provider:deleteApiKey', providerId);
|
||||
const result = await hostApiFetch<{ success: boolean; error?: string }>(
|
||||
`/api/providers/${encodeURIComponent(providerId)}?apiKeyOnly=1`,
|
||||
{ method: 'DELETE' },
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to delete API key');
|
||||
}
|
||||
|
||||
// Refresh the list
|
||||
await get().fetchProviders();
|
||||
await get().refreshProviderSnapshot();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete API key:', error);
|
||||
throw error;
|
||||
@@ -180,38 +285,62 @@ export const useProviderStore = create<ProviderState>((set, get) => ({
|
||||
|
||||
setDefaultProvider: async (providerId) => {
|
||||
try {
|
||||
const result = await invokeIpc<{ success: boolean; error?: string }>('provider:setDefault', providerId);
|
||||
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/providers/default', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ providerId }),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to set default provider');
|
||||
}
|
||||
|
||||
set({ defaultProviderId: providerId });
|
||||
set({ defaultAccountId: providerId });
|
||||
} catch (error) {
|
||||
console.error('Failed to set default provider:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
validateApiKey: async (providerId, apiKey, options) => {
|
||||
|
||||
setDefaultAccount: async (accountId) => {
|
||||
try {
|
||||
const result = await invokeIpc<{ valid: boolean; error?: string }>(
|
||||
'provider:validateKey',
|
||||
providerId,
|
||||
apiKey,
|
||||
options
|
||||
);
|
||||
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/provider-accounts/default', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ accountId }),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to set default provider account');
|
||||
}
|
||||
|
||||
set({ defaultAccountId: accountId });
|
||||
} catch (error) {
|
||||
console.error('Failed to set default account:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
validateAccountApiKey: async (providerId, apiKey, options) => {
|
||||
try {
|
||||
const result = await hostApiFetch<{ valid: boolean; error?: string }>('/api/providers/validate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ providerId, apiKey, options }),
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
return { valid: false, error: String(error) };
|
||||
}
|
||||
},
|
||||
|
||||
validateApiKey: async (providerId, apiKey, options) => get().validateAccountApiKey(providerId, apiKey, options),
|
||||
|
||||
getApiKey: async (providerId) => {
|
||||
getAccountApiKey: async (providerId) => {
|
||||
try {
|
||||
return await invokeIpc<string | null>('provider:getApiKey', providerId);
|
||||
const result = await hostApiFetch<{ apiKey: string | null }>(`/api/providers/${encodeURIComponent(providerId)}/api-key`);
|
||||
return result.apiKey;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getApiKey: async (providerId) => get().getAccountApiKey(providerId),
|
||||
}));
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import i18n from '@/i18n';
|
||||
import { hostApiFetch } from '@/lib/host-api';
|
||||
import { invokeIpc } from '@/lib/api-client';
|
||||
|
||||
type Theme = 'light' | 'dark' | 'system';
|
||||
@@ -99,7 +100,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
|
||||
init: async () => {
|
||||
try {
|
||||
const settings = await invokeIpc<Partial<typeof defaultSettings>>('settings:getAll');
|
||||
const settings = await hostApiFetch<Partial<typeof defaultSettings>>('/api/settings');
|
||||
set((state) => ({ ...state, ...settings }));
|
||||
if (settings.language) {
|
||||
i18n.changeLanguage(settings.language);
|
||||
@@ -111,11 +112,30 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
},
|
||||
|
||||
setTheme: (theme) => set({ theme }),
|
||||
setLanguage: (language) => { i18n.changeLanguage(language); set({ language }); void invokeIpc('settings:set', 'language', language).catch(() => {}); },
|
||||
setLanguage: (language) => {
|
||||
i18n.changeLanguage(language);
|
||||
set({ language });
|
||||
void hostApiFetch('/api/settings/language', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ value: language }),
|
||||
}).catch(() => {});
|
||||
},
|
||||
setStartMinimized: (startMinimized) => set({ startMinimized }),
|
||||
setLaunchAtStartup: (launchAtStartup) => set({ launchAtStartup }),
|
||||
setGatewayAutoStart: (gatewayAutoStart) => { set({ gatewayAutoStart }); void invokeIpc('settings:set', 'gatewayAutoStart', gatewayAutoStart).catch(() => {}); },
|
||||
setGatewayPort: (gatewayPort) => { set({ gatewayPort }); void invokeIpc('settings:set', 'gatewayPort', gatewayPort).catch(() => {}); },
|
||||
setGatewayAutoStart: (gatewayAutoStart) => {
|
||||
set({ gatewayAutoStart });
|
||||
void hostApiFetch('/api/settings/gatewayAutoStart', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ value: gatewayAutoStart }),
|
||||
}).catch(() => {});
|
||||
},
|
||||
setGatewayPort: (gatewayPort) => {
|
||||
set({ gatewayPort });
|
||||
void hostApiFetch('/api/settings/gatewayPort', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ value: gatewayPort }),
|
||||
}).catch(() => {});
|
||||
},
|
||||
setProxyEnabled: (proxyEnabled) => set({ proxyEnabled }),
|
||||
setProxyServer: (proxyServer) => set({ proxyServer }),
|
||||
setProxyHttpServer: (proxyHttpServer) => set({ proxyHttpServer }),
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
* Manages skill/plugin state
|
||||
*/
|
||||
import { create } from 'zustand';
|
||||
import { hostApiFetch } from '@/lib/host-api';
|
||||
import { useGatewayStore } from './gateway';
|
||||
import type { Skill, MarketplaceSkill } from '../types/skill';
|
||||
import { invokeIpc } from '@/lib/api-client';
|
||||
|
||||
type GatewaySkillStatus = {
|
||||
skillKey: string;
|
||||
@@ -24,12 +25,6 @@ type GatewaySkillsStatusResult = {
|
||||
skills?: GatewaySkillStatus[];
|
||||
};
|
||||
|
||||
type GatewayRpcResponse<T> = {
|
||||
success: boolean;
|
||||
result?: T;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type ClawHubListResult = {
|
||||
slug: string;
|
||||
version?: string;
|
||||
@@ -71,27 +66,20 @@ export const useSkillsStore = create<SkillsState>((set, get) => ({
|
||||
}
|
||||
try {
|
||||
// 1. Fetch from Gateway (running skills)
|
||||
const gatewayResult = await invokeIpc<GatewayRpcResponse<GatewaySkillsStatusResult>>(
|
||||
'gateway:rpc',
|
||||
'skills.status'
|
||||
);
|
||||
const gatewayData = await useGatewayStore.getState().rpc<GatewaySkillsStatusResult>('skills.status');
|
||||
|
||||
// 2. Fetch from ClawHub (installed on disk)
|
||||
const clawhubResult = await invokeIpc<{ success: boolean; results?: ClawHubListResult[]; error?: string }>(
|
||||
'clawhub:list'
|
||||
);
|
||||
const clawhubResult = await hostApiFetch<{ success: boolean; results?: ClawHubListResult[]; error?: string }>('/api/clawhub/list');
|
||||
|
||||
// 3. Fetch configurations directly from Electron (since Gateway doesn't return them)
|
||||
const configResult = await invokeIpc<Record<string, { apiKey?: string; env?: Record<string, string> }>>(
|
||||
'skill:getAllConfigs'
|
||||
);
|
||||
const configResult = await hostApiFetch<Record<string, { apiKey?: string; env?: Record<string, string> }>>('/api/skills/configs');
|
||||
|
||||
let combinedSkills: Skill[] = [];
|
||||
const currentSkills = get().skills;
|
||||
|
||||
// Map gateway skills info
|
||||
if (gatewayResult.success && gatewayResult.result?.skills) {
|
||||
combinedSkills = gatewayResult.result.skills.map((s: GatewaySkillStatus) => {
|
||||
if (gatewayData.skills) {
|
||||
combinedSkills = gatewayData.skills.map((s: GatewaySkillStatus) => {
|
||||
// Merge with direct config if available
|
||||
const directConfig = configResult[s.skillKey] || {};
|
||||
|
||||
@@ -156,7 +144,10 @@ export const useSkillsStore = create<SkillsState>((set, get) => ({
|
||||
searchSkills: async (query: string) => {
|
||||
set({ searching: true, searchError: null });
|
||||
try {
|
||||
const result = await invokeIpc<{ success: boolean; results?: MarketplaceSkill[]; error?: string }>('clawhub:search', { query });
|
||||
const result = await hostApiFetch<{ success: boolean; results?: MarketplaceSkill[]; error?: string }>('/api/clawhub/search', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ query }),
|
||||
});
|
||||
if (result.success) {
|
||||
set({ searchResults: result.results || [] });
|
||||
} else {
|
||||
@@ -178,7 +169,10 @@ export const useSkillsStore = create<SkillsState>((set, get) => ({
|
||||
installSkill: async (slug: string, version?: string) => {
|
||||
set((state) => ({ installing: { ...state.installing, [slug]: true } }));
|
||||
try {
|
||||
const result = await invokeIpc<{ success: boolean; error?: string }>('clawhub:install', { slug, version });
|
||||
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/clawhub/install', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ slug, version }),
|
||||
});
|
||||
if (!result.success) {
|
||||
if (result.error?.includes('Timeout')) {
|
||||
throw new Error('installTimeoutError');
|
||||
@@ -205,7 +199,10 @@ export const useSkillsStore = create<SkillsState>((set, get) => ({
|
||||
uninstallSkill: async (slug: string) => {
|
||||
set((state) => ({ installing: { ...state.installing, [slug]: true } }));
|
||||
try {
|
||||
const result = await invokeIpc<{ success: boolean; error?: string }>('clawhub:uninstall', { slug });
|
||||
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/clawhub/uninstall', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ slug }),
|
||||
});
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Uninstall failed');
|
||||
}
|
||||
@@ -227,17 +224,8 @@ export const useSkillsStore = create<SkillsState>((set, get) => ({
|
||||
const { updateSkill } = get();
|
||||
|
||||
try {
|
||||
const result = await invokeIpc<GatewayRpcResponse<unknown>>(
|
||||
'gateway:rpc',
|
||||
'skills.update',
|
||||
{ skillKey: skillId, enabled: true }
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
updateSkill(skillId, { enabled: true });
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to enable skill');
|
||||
}
|
||||
await useGatewayStore.getState().rpc('skills.update', { skillKey: skillId, enabled: true });
|
||||
updateSkill(skillId, { enabled: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to enable skill:', error);
|
||||
throw error;
|
||||
@@ -253,17 +241,8 @@ export const useSkillsStore = create<SkillsState>((set, get) => ({
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await invokeIpc<GatewayRpcResponse<unknown>>(
|
||||
'gateway:rpc',
|
||||
'skills.update',
|
||||
{ skillKey: skillId, enabled: false }
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
updateSkill(skillId, { enabled: false });
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to disable skill');
|
||||
}
|
||||
await useGatewayStore.getState().rpc('skills.update', { skillKey: skillId, enabled: false });
|
||||
updateSkill(skillId, { enabled: false });
|
||||
} catch (error) {
|
||||
console.error('Failed to disable skill:', error);
|
||||
throw error;
|
||||
@@ -279,4 +258,4 @@ export const useSkillsStore = create<SkillsState>((set, get) => ({
|
||||
),
|
||||
}));
|
||||
},
|
||||
}));
|
||||
}));
|
||||
Reference in New Issue
Block a user