feat(phase2): complete P1 tasks - Channels, Triggers, Skills CRUD and UI enhancements
Phase 2 P1 Tasks Completed: API Layer (gateway-client.ts, gatewayStore.ts): - Add Channels CRUD: getChannel, createChannel, updateChannel, deleteChannel - Add Triggers CRUD: getTrigger, createTrigger, updateTrigger, deleteTrigger - Add Skills CRUD: getSkill, createSkill, updateSkill, deleteSkill - Add Scheduled Tasks API: createScheduledTask, deleteScheduledTask, toggleScheduledTask - Add loadModels action for dynamic model list UI Components: - ModelsAPI.tsx: Dynamic model loading from API with loading/error states - SchedulerPanel.tsx: Full CreateJobModal with cron/interval/once scheduling - SecurityStatus.tsx: Loading states, error handling, retry functionality - WorkflowEditor.tsx: New workflow creation/editing modal (new file) - WorkflowHistory.tsx: Workflow execution history viewer (new file) - WorkflowList.tsx: Integrated editor and history access Configuration: - Add 4 Hands TOML configs: clip, collector, predictor, twitter Documentation (SYSTEM_ANALYSIS.md): - Update API coverage: 65% → 89% (53/62 endpoints) - Update UI completion: 85% → 92% - Mark Phase 2 P1 tasks as completed - Update technical debt cleanup status Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { create } from 'zustand';
|
||||
import { DEFAULT_GATEWAY_URL, FALLBACK_GATEWAY_URLS, GatewayClient, ConnectionState, getGatewayClient, getLocalDeviceIdentity, getStoredGatewayToken, getStoredGatewayUrl, setStoredGatewayToken, setStoredGatewayUrl } from '../lib/gateway-client';
|
||||
import type { GatewayModelChoice } from '../lib/gateway-config';
|
||||
import { approveLocalGatewayDevicePairing, getLocalGatewayAuth, getLocalGatewayStatus, getUnsupportedLocalGatewayStatus, isTauriRuntime, prepareLocalGatewayForTauri, restartLocalGateway as restartLocalGatewayCommand, startLocalGateway as startLocalGatewayCommand, stopLocalGateway as stopLocalGatewayCommand, type LocalGatewayStatus } from '../lib/tauri-gateway';
|
||||
import { useChatStore } from './chatStore';
|
||||
|
||||
@@ -59,6 +60,10 @@ interface SkillInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
source: 'builtin' | 'extra';
|
||||
description?: string;
|
||||
triggers?: Array<{ type: string; pattern?: string }>;
|
||||
actions?: Array<{ type: string; params?: Record<string, unknown> }>;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
interface QuickConfig {
|
||||
@@ -120,7 +125,16 @@ export interface Hand {
|
||||
export interface HandRun {
|
||||
runId: string;
|
||||
status: string;
|
||||
startedAt: string;
|
||||
completedAt?: string;
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface HandRunStore {
|
||||
runs: HandRun[];
|
||||
isLoading: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface Workflow {
|
||||
@@ -128,6 +142,7 @@ export interface Workflow {
|
||||
name: string;
|
||||
steps: number;
|
||||
description?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowRun {
|
||||
@@ -137,6 +152,26 @@ export interface WorkflowRun {
|
||||
result?: unknown;
|
||||
}
|
||||
|
||||
// === Session Types ===
|
||||
|
||||
export interface SessionMessage {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
createdAt: string;
|
||||
tokens?: { input?: number; output?: number };
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
id: string;
|
||||
agentId: string;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
messageCount?: number;
|
||||
status?: 'active' | 'archived' | 'expired';
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface Trigger {
|
||||
id: string;
|
||||
type: string;
|
||||
@@ -277,13 +312,26 @@ interface GatewayStore {
|
||||
quickConfig: QuickConfig;
|
||||
workspaceInfo: WorkspaceInfo | null;
|
||||
|
||||
// Models Data
|
||||
models: GatewayModelChoice[];
|
||||
modelsLoading: boolean;
|
||||
modelsError: string | null;
|
||||
|
||||
// OpenFang Data
|
||||
hands: Hand[];
|
||||
handRuns: Record<string, HandRun[]>; // handName -> runs
|
||||
workflows: Workflow[];
|
||||
triggers: Trigger[];
|
||||
auditLogs: AuditLogEntry[];
|
||||
securityStatus: SecurityStatus | null;
|
||||
securityStatusLoading: boolean;
|
||||
securityStatusError: string | null;
|
||||
approvals: Approval[];
|
||||
// Session Data
|
||||
sessions: Session[];
|
||||
sessionMessages: Record<string, SessionMessage[]>; // sessionId -> messages
|
||||
// Workflow Runs Data
|
||||
workflowRuns: Record<string, WorkflowRun[]>; // workflowId -> runs
|
||||
|
||||
// Client reference
|
||||
client: GatewayClient;
|
||||
@@ -310,8 +358,27 @@ interface GatewayStore {
|
||||
loadUsageStats: () => Promise<void>;
|
||||
loadPluginStatus: () => Promise<void>;
|
||||
loadChannels: () => Promise<void>;
|
||||
getChannel: (id: string) => Promise<ChannelInfo | undefined>;
|
||||
createChannel: (channel: { type: string; name: string; config: Record<string, unknown>; enabled?: boolean }) => Promise<ChannelInfo | undefined>;
|
||||
updateChannel: (id: string, updates: { name?: string; config?: Record<string, unknown>; enabled?: boolean }) => Promise<ChannelInfo | undefined>;
|
||||
deleteChannel: (id: string) => Promise<void>;
|
||||
loadScheduledTasks: () => Promise<void>;
|
||||
createScheduledTask: (task: {
|
||||
name: string;
|
||||
schedule: string;
|
||||
scheduleType: 'cron' | 'interval' | 'once';
|
||||
target?: {
|
||||
type: 'agent' | 'hand' | 'workflow';
|
||||
id: string;
|
||||
};
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
}) => Promise<ScheduledTask | undefined>;
|
||||
loadSkillsCatalog: () => Promise<void>;
|
||||
getSkill: (id: string) => Promise<SkillInfo | undefined>;
|
||||
createSkill: (skill: { name: string; description?: string; triggers: Array<{ type: string; pattern?: string }>; actions: Array<{ type: string; params?: Record<string, unknown> }>; enabled?: boolean }) => Promise<SkillInfo | undefined>;
|
||||
updateSkill: (id: string, updates: { name?: string; description?: string; triggers?: Array<{ type: string; pattern?: string }>; actions?: Array<{ type: string; params?: Record<string, unknown> }>; enabled?: boolean }) => Promise<SkillInfo | undefined>;
|
||||
deleteSkill: (id: string) => Promise<void>;
|
||||
loadQuickConfig: () => Promise<void>;
|
||||
saveQuickConfig: (updates: Partial<QuickConfig>) => Promise<void>;
|
||||
loadWorkspaceInfo: () => Promise<void>;
|
||||
@@ -321,20 +388,59 @@ interface GatewayStore {
|
||||
restartLocalGateway: () => Promise<LocalGatewayStatus | undefined>;
|
||||
clearLogs: () => void;
|
||||
|
||||
// Models Actions
|
||||
loadModels: () => Promise<void>;
|
||||
|
||||
// OpenFang Actions
|
||||
loadHands: () => Promise<void>;
|
||||
getHandDetails: (name: string) => Promise<Hand | undefined>;
|
||||
loadHandRuns: (name: string, opts?: { limit?: number; offset?: number }) => Promise<HandRun[]>;
|
||||
triggerHand: (name: string, params?: Record<string, unknown>) => Promise<HandRun | undefined>;
|
||||
approveHand: (name: string, runId: string, approved: boolean, reason?: string) => Promise<void>;
|
||||
cancelHand: (name: string, runId: string) => Promise<void>;
|
||||
loadWorkflows: () => Promise<void>;
|
||||
createWorkflow: (workflow: {
|
||||
name: string;
|
||||
description?: string;
|
||||
steps: Array<{
|
||||
handName: string;
|
||||
name?: string;
|
||||
params?: Record<string, unknown>;
|
||||
condition?: string;
|
||||
}>;
|
||||
}) => Promise<Workflow | undefined>;
|
||||
updateWorkflow: (id: string, updates: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
steps?: Array<{
|
||||
handName: string;
|
||||
name?: string;
|
||||
params?: Record<string, unknown>;
|
||||
condition?: string;
|
||||
}>;
|
||||
}) => Promise<Workflow | undefined>;
|
||||
deleteWorkflow: (id: string) => Promise<void>;
|
||||
executeWorkflow: (id: string, input?: Record<string, unknown>) => Promise<WorkflowRun | undefined>;
|
||||
cancelWorkflow: (id: string, runId: string) => Promise<void>;
|
||||
loadTriggers: () => Promise<void>;
|
||||
// Workflow Run Actions
|
||||
loadWorkflowRuns: (workflowId: string, opts?: { limit?: number; offset?: number }) => Promise<WorkflowRun[]>;
|
||||
loadTriggers: () => Promise<void>;
|
||||
// Trigger Actions
|
||||
getTrigger: (id: string) => Promise<Trigger | undefined>;
|
||||
createTrigger: (trigger: { type: string; name?: string; enabled?: boolean; config?: Record<string, unknown>; handName?: string; workflowId?: string }) => Promise<Trigger | undefined>;
|
||||
updateTrigger: (id: string, updates: { name?: string; enabled?: boolean; config?: Record<string, unknown>; handName?: string; workflowId?: string }) => Promise<Trigger | undefined>;
|
||||
deleteTrigger: (id: string) => Promise<void>;
|
||||
loadAuditLogs: (opts?: { limit?: number; offset?: number }) => Promise<void>;
|
||||
loadSecurityStatus: () => Promise<void>;
|
||||
loadApprovals: (status?: ApprovalStatus) => Promise<void>;
|
||||
respondToApproval: (approvalId: string, approved: boolean, reason?: string) => Promise<void>;
|
||||
// Session Actions
|
||||
loadSessions: (opts?: { limit?: number; offset?: number }) => Promise<void>;
|
||||
getSession: (sessionId: string) => Promise<Session | undefined>;
|
||||
createSession: (agentId: string, metadata?: Record<string, unknown>) => Promise<Session | undefined>;
|
||||
deleteSession: (sessionId: string) => Promise<void>;
|
||||
loadSessionMessages: (sessionId: string, opts?: { limit?: number; offset?: number }) => Promise<SessionMessage[]>;
|
||||
}
|
||||
|
||||
function normalizeGatewayUrlCandidate(url: string): string {
|
||||
@@ -381,13 +487,25 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
skillsCatalog: [],
|
||||
quickConfig: {},
|
||||
workspaceInfo: null,
|
||||
// Models state
|
||||
models: [],
|
||||
modelsLoading: false,
|
||||
modelsError: null,
|
||||
// OpenFang state
|
||||
hands: [],
|
||||
handRuns: {}, // handName -> runs
|
||||
workflows: [],
|
||||
triggers: [],
|
||||
auditLogs: [],
|
||||
securityStatus: null,
|
||||
securityStatusLoading: false,
|
||||
securityStatusError: null,
|
||||
approvals: [],
|
||||
// Session state
|
||||
sessions: [],
|
||||
sessionMessages: {},
|
||||
// Workflow Runs state
|
||||
workflowRuns: {},
|
||||
client,
|
||||
|
||||
connect: async (url?: string, token?: string) => {
|
||||
@@ -630,6 +748,73 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
set({ channels });
|
||||
},
|
||||
|
||||
getChannel: async (id: string) => {
|
||||
try {
|
||||
const result = await get().client.getChannel(id);
|
||||
if (result?.channel) {
|
||||
// Update the channel in the local state if it exists
|
||||
const currentChannels = get().channels;
|
||||
const existingIndex = currentChannels.findIndex(c => c.id === id);
|
||||
if (existingIndex >= 0) {
|
||||
const updatedChannels = [...currentChannels];
|
||||
updatedChannels[existingIndex] = result.channel;
|
||||
set({ channels: updatedChannels });
|
||||
}
|
||||
return result.channel as ChannelInfo;
|
||||
}
|
||||
return undefined;
|
||||
} catch (err: any) {
|
||||
set({ error: err.message });
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
createChannel: async (channel) => {
|
||||
try {
|
||||
const result = await get().client.createChannel(channel);
|
||||
if (result?.channel) {
|
||||
// Add the new channel to local state
|
||||
const currentChannels = get().channels;
|
||||
set({ channels: [...currentChannels, result.channel as ChannelInfo] });
|
||||
return result.channel as ChannelInfo;
|
||||
}
|
||||
return undefined;
|
||||
} catch (err: any) {
|
||||
set({ error: err.message });
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
updateChannel: async (id, updates) => {
|
||||
try {
|
||||
const result = await get().client.updateChannel(id, updates);
|
||||
if (result?.channel) {
|
||||
// Update the channel in local state
|
||||
const currentChannels = get().channels;
|
||||
const updatedChannels = currentChannels.map(c =>
|
||||
c.id === id ? (result.channel as ChannelInfo) : c
|
||||
);
|
||||
set({ channels: updatedChannels });
|
||||
return result.channel as ChannelInfo;
|
||||
}
|
||||
return undefined;
|
||||
} catch (err: any) {
|
||||
set({ error: err.message });
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
deleteChannel: async (id) => {
|
||||
try {
|
||||
await get().client.deleteChannel(id);
|
||||
// Remove the channel from local state
|
||||
const currentChannels = get().channels;
|
||||
set({ channels: currentChannels.filter(c => c.id !== id) });
|
||||
} catch (err: any) {
|
||||
set({ error: err.message });
|
||||
}
|
||||
},
|
||||
|
||||
loadScheduledTasks: async () => {
|
||||
try {
|
||||
const result = await get().client.listScheduledTasks();
|
||||
@@ -637,6 +822,26 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
} catch { /* ignore if heartbeat.tasks not available */ }
|
||||
},
|
||||
|
||||
createScheduledTask: async (task) => {
|
||||
try {
|
||||
const result = await get().client.createScheduledTask(task);
|
||||
const newTask = {
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
schedule: result.schedule,
|
||||
status: result.status as 'active' | 'paused' | 'completed' | 'error',
|
||||
};
|
||||
set((state) => ({
|
||||
scheduledTasks: [...state.scheduledTasks, newTask],
|
||||
}));
|
||||
return newTask;
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to create scheduled task';
|
||||
set({ error: errorMessage });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
loadSkillsCatalog: async () => {
|
||||
try {
|
||||
const result = await get().client.listSkills();
|
||||
@@ -652,6 +857,56 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
} catch { /* ignore if skills list not available */ }
|
||||
},
|
||||
|
||||
getSkill: async (id: string) => {
|
||||
try {
|
||||
const result = await get().client.getSkill(id);
|
||||
return result?.skill as SkillInfo | undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
createSkill: async (skill) => {
|
||||
try {
|
||||
const result = await get().client.createSkill(skill);
|
||||
const newSkill = result?.skill as SkillInfo | undefined;
|
||||
if (newSkill) {
|
||||
set((state) => ({
|
||||
skillsCatalog: [...state.skillsCatalog, newSkill],
|
||||
}));
|
||||
}
|
||||
return newSkill;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
updateSkill: async (id, updates) => {
|
||||
try {
|
||||
const result = await get().client.updateSkill(id, updates);
|
||||
const updatedSkill = result?.skill as SkillInfo | undefined;
|
||||
if (updatedSkill) {
|
||||
set((state) => ({
|
||||
skillsCatalog: state.skillsCatalog.map((s) =>
|
||||
s.id === id ? updatedSkill : s
|
||||
),
|
||||
}));
|
||||
}
|
||||
return updatedSkill;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
deleteSkill: async (id) => {
|
||||
try {
|
||||
await get().client.deleteSkill(id);
|
||||
set((state) => ({
|
||||
skillsCatalog: state.skillsCatalog.filter((s) => s.id !== id),
|
||||
}));
|
||||
} catch { /* ignore deletion errors */ }
|
||||
},
|
||||
|
||||
loadQuickConfig: async () => {
|
||||
try {
|
||||
const result = await get().client.getQuickConfig();
|
||||
@@ -841,6 +1096,27 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
}
|
||||
},
|
||||
|
||||
loadHandRuns: async (name: string, opts?: { limit?: number; offset?: number }) => {
|
||||
try {
|
||||
const result = await get().client.listHandRuns(name, opts);
|
||||
const runs: HandRun[] = (result?.runs || []).map((r: any) => ({
|
||||
runId: r.runId || r.run_id || r.id,
|
||||
status: r.status || 'unknown',
|
||||
startedAt: r.startedAt || r.started_at || r.created_at || new Date().toISOString(),
|
||||
completedAt: r.completedAt || r.completed_at || r.finished_at,
|
||||
result: r.result || r.output,
|
||||
error: r.error || r.message,
|
||||
}));
|
||||
// Store runs by hand name
|
||||
set(state => ({
|
||||
handRuns: { ...state.handRuns, [name]: runs },
|
||||
}));
|
||||
return runs;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
triggerHand: async (name: string, params?: Record<string, unknown>) => {
|
||||
try {
|
||||
const result = await get().client.triggerHand(name, params);
|
||||
@@ -884,6 +1160,81 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
}
|
||||
},
|
||||
|
||||
createWorkflow: async (workflow: {
|
||||
name: string;
|
||||
description?: string;
|
||||
steps: Array<{
|
||||
handName: string;
|
||||
name?: string;
|
||||
params?: Record<string, unknown>;
|
||||
condition?: string;
|
||||
}>;
|
||||
}) => {
|
||||
try {
|
||||
const result = await get().client.createWorkflow(workflow);
|
||||
if (result) {
|
||||
const newWorkflow: Workflow = {
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
steps: workflow.steps.length,
|
||||
description: workflow.description,
|
||||
};
|
||||
set(state => ({ workflows: [...state.workflows, newWorkflow] }));
|
||||
return newWorkflow;
|
||||
}
|
||||
return undefined;
|
||||
} catch (err: any) {
|
||||
set({ error: err.message });
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
updateWorkflow: async (id: string, updates: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
steps?: Array<{
|
||||
handName: string;
|
||||
name?: string;
|
||||
params?: Record<string, unknown>;
|
||||
condition?: string;
|
||||
}>;
|
||||
}) => {
|
||||
try {
|
||||
const result = await get().client.updateWorkflow(id, updates);
|
||||
if (result) {
|
||||
set(state => ({
|
||||
workflows: state.workflows.map(w =>
|
||||
w.id === id
|
||||
? {
|
||||
...w,
|
||||
name: updates.name || w.name,
|
||||
description: updates.description ?? w.description,
|
||||
steps: updates.steps?.length ?? w.steps,
|
||||
}
|
||||
: w
|
||||
),
|
||||
}));
|
||||
return get().workflows.find(w => w.id === id);
|
||||
}
|
||||
return undefined;
|
||||
} catch (err: any) {
|
||||
set({ error: err.message });
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
deleteWorkflow: async (id: string) => {
|
||||
try {
|
||||
await get().client.deleteWorkflow(id);
|
||||
set(state => ({
|
||||
workflows: state.workflows.filter(w => w.id !== id),
|
||||
}));
|
||||
} catch (err: any) {
|
||||
set({ error: err.message });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
executeWorkflow: async (id: string, input?: Record<string, unknown>) => {
|
||||
try {
|
||||
const result = await get().client.executeWorkflow(id, input);
|
||||
@@ -912,6 +1263,64 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
} catch { /* ignore if triggers API not available */ }
|
||||
},
|
||||
|
||||
getTrigger: async (id: string) => {
|
||||
try {
|
||||
const result = await get().client.getTrigger(id);
|
||||
if (!result) return undefined;
|
||||
return {
|
||||
id: result.id,
|
||||
type: result.type,
|
||||
enabled: result.enabled,
|
||||
} as Trigger;
|
||||
} catch (err: any) {
|
||||
set({ error: err.message });
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
createTrigger: async (trigger) => {
|
||||
try {
|
||||
const result = await get().client.createTrigger(trigger);
|
||||
if (!result?.id) return undefined;
|
||||
// Refresh triggers list after creation
|
||||
await get().loadTriggers();
|
||||
return get().triggers.find(t => t.id === result.id);
|
||||
} catch (err: any) {
|
||||
set({ error: err.message });
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
updateTrigger: async (id: string, updates) => {
|
||||
try {
|
||||
await get().client.updateTrigger(id, updates);
|
||||
// Update local state
|
||||
set(state => ({
|
||||
triggers: state.triggers.map(t =>
|
||||
t.id === id
|
||||
? { ...t, ...updates }
|
||||
: t
|
||||
),
|
||||
}));
|
||||
return get().triggers.find(t => t.id === id);
|
||||
} catch (err: any) {
|
||||
set({ error: err.message });
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
deleteTrigger: async (id: string) => {
|
||||
try {
|
||||
await get().client.deleteTrigger(id);
|
||||
set(state => ({
|
||||
triggers: state.triggers.filter(t => t.id !== id),
|
||||
}));
|
||||
} catch (err: any) {
|
||||
set({ error: err.message });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
loadAuditLogs: async (opts?: { limit?: number; offset?: number }) => {
|
||||
try {
|
||||
const result = await get().client.getAuditLogs(opts);
|
||||
@@ -920,6 +1329,7 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
},
|
||||
|
||||
loadSecurityStatus: async () => {
|
||||
set({ securityStatusLoading: true, securityStatusError: null });
|
||||
try {
|
||||
const result = await get().client.getSecurityStatus();
|
||||
if (result?.layers) {
|
||||
@@ -934,9 +1344,21 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
totalCount,
|
||||
securityLevel,
|
||||
},
|
||||
securityStatusLoading: false,
|
||||
securityStatusError: null,
|
||||
});
|
||||
} else {
|
||||
set({
|
||||
securityStatusLoading: false,
|
||||
securityStatusError: 'API returned no data',
|
||||
});
|
||||
}
|
||||
} catch { /* ignore if security API not available */ }
|
||||
} catch (err: any) {
|
||||
set({
|
||||
securityStatusLoading: false,
|
||||
securityStatusError: err.message || 'Security API not available',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
loadApprovals: async (status?: ApprovalStatus) => {
|
||||
@@ -971,7 +1393,161 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
}
|
||||
},
|
||||
|
||||
// === Session Actions ===
|
||||
|
||||
loadSessions: async (opts?: { limit?: number; offset?: number }) => {
|
||||
try {
|
||||
const result = await get().client.listSessions(opts);
|
||||
const sessions: Session[] = (result?.sessions || []).map((s: any) => ({
|
||||
id: s.id,
|
||||
agentId: s.agent_id,
|
||||
createdAt: s.created_at,
|
||||
updatedAt: s.updated_at,
|
||||
messageCount: s.message_count,
|
||||
status: s.status,
|
||||
metadata: s.metadata,
|
||||
}));
|
||||
set({ sessions });
|
||||
} catch {
|
||||
/* ignore if sessions API not available */
|
||||
}
|
||||
},
|
||||
|
||||
getSession: async (sessionId: string) => {
|
||||
try {
|
||||
const result = await get().client.getSession(sessionId);
|
||||
if (!result) return undefined;
|
||||
const session: Session = {
|
||||
id: result.id,
|
||||
agentId: result.agent_id,
|
||||
createdAt: result.created_at,
|
||||
updatedAt: result.updated_at,
|
||||
messageCount: result.message_count,
|
||||
status: result.status,
|
||||
metadata: result.metadata,
|
||||
};
|
||||
// Update in list if exists
|
||||
set(state => ({
|
||||
sessions: state.sessions.some(s => s.id === sessionId)
|
||||
? state.sessions.map(s => s.id === sessionId ? session : s)
|
||||
: [...state.sessions, session],
|
||||
}));
|
||||
return session;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
createSession: async (agentId: string, metadata?: Record<string, unknown>) => {
|
||||
try {
|
||||
const result = await get().client.createSession({ agent_id: agentId, metadata });
|
||||
if (!result) return undefined;
|
||||
const session: Session = {
|
||||
id: result.id,
|
||||
agentId: result.agent_id,
|
||||
createdAt: result.created_at,
|
||||
status: 'active',
|
||||
metadata: result.metadata,
|
||||
};
|
||||
set(state => ({ sessions: [...state.sessions, session] }));
|
||||
return session;
|
||||
} catch (err: any) {
|
||||
set({ error: err.message });
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
deleteSession: async (sessionId: string) => {
|
||||
try {
|
||||
await get().client.deleteSession(sessionId);
|
||||
set(state => ({
|
||||
sessions: state.sessions.filter(s => s.id !== sessionId),
|
||||
sessionMessages: Object.fromEntries(
|
||||
Object.entries(state.sessionMessages).filter(([id]) => id !== sessionId)
|
||||
),
|
||||
}));
|
||||
} catch (err: any) {
|
||||
set({ error: err.message });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
loadSessionMessages: async (sessionId: string, opts?: { limit?: number; offset?: number }) => {
|
||||
try {
|
||||
const result = await get().client.getSessionMessages(sessionId, opts);
|
||||
const messages: SessionMessage[] = (result?.messages || []).map((m: any) => ({
|
||||
id: m.id,
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
createdAt: m.created_at,
|
||||
tokens: m.tokens,
|
||||
}));
|
||||
set(state => ({
|
||||
sessionMessages: { ...state.sessionMessages, [sessionId]: messages },
|
||||
}));
|
||||
return messages;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
loadWorkflowRuns: async (workflowId: string, opts?: { limit?: number; offset?: number }) => {
|
||||
try {
|
||||
const result = await get().client.listWorkflowRuns(workflowId, opts);
|
||||
const runs: WorkflowRun[] = (result?.runs || []).map((r: any) => ({
|
||||
runId: r.runId || r.run_id || r.id,
|
||||
status: r.status || 'unknown',
|
||||
step: r.step,
|
||||
result: r.result || r.output,
|
||||
}));
|
||||
set(state => ({
|
||||
workflowRuns: { ...state.workflowRuns, [workflowId]: runs },
|
||||
}));
|
||||
return runs;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
clearLogs: () => set({ logs: [] }),
|
||||
|
||||
// === Models Actions ===
|
||||
|
||||
loadModels: async () => {
|
||||
try {
|
||||
set({ modelsLoading: true, modelsError: null });
|
||||
const result = await get().client.listModels();
|
||||
const models: GatewayModelChoice[] = result?.models || [];
|
||||
set({ models, modelsLoading: false });
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to load models';
|
||||
set({ modelsError: message, modelsLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// === Workflow Run Actions ===
|
||||
|
||||
loadWorkflowRuns: async (workflowId: string, opts?: { limit?: number; offset?: number }) => {
|
||||
try {
|
||||
const result = await get().client.listWorkflowRuns(workflowId, opts);
|
||||
const runs: WorkflowRun[] = (result?.runs || []).map((r: any) => ({
|
||||
runId: r.runId || r.run_id,
|
||||
status: r.status,
|
||||
startedAt: r.startedAt || r.started_at,
|
||||
completedAt: r.completedAt || r.completed_at,
|
||||
step: r.step,
|
||||
result: r.result,
|
||||
error: r.error,
|
||||
}));
|
||||
// Store runs by workflow ID
|
||||
set(state => ({
|
||||
workflowRuns: { ...state.workflowRuns, [workflowId]: runs },
|
||||
}));
|
||||
return runs;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user