refactor(phase-11): extract specialized stores from gatewayStore
Decompose monolithic gatewayStore.ts (1660 lines) into focused stores: - connectionStore.ts (444 lines) - WebSocket, auth, local gateway - agentStore.ts (256 lines) - Clones, usage stats, plugins - handStore.ts (498 lines) - Hands, triggers, approvals - workflowStore.ts (255 lines) - Workflows, runs - configStore.ts (537 lines) - QuickConfig, channels, skills Each store uses client injection pattern for loose coupling. Coordinator layer to be added in next commit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
498
desktop/src/store/handStore.ts
Normal file
498
desktop/src/store/handStore.ts
Normal file
@@ -0,0 +1,498 @@
|
||||
/**
|
||||
* handStore.ts - Hand, Trigger, and Approval management store
|
||||
*
|
||||
* Extracted from gatewayStore.ts for Phase 11 Store Refactoring.
|
||||
* Manages OpenFang Hands, Triggers, and Approval workflows.
|
||||
*/
|
||||
import { create } from 'zustand';
|
||||
import type { GatewayClient } from '../lib/gateway-client';
|
||||
|
||||
// === Re-exported Types (from gatewayStore for compatibility) ===
|
||||
|
||||
export interface HandRequirement {
|
||||
description: string;
|
||||
met: boolean;
|
||||
details?: string;
|
||||
}
|
||||
|
||||
export interface Hand {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
status: 'idle' | 'running' | 'needs_approval' | 'error' | 'unavailable' | 'setup_needed';
|
||||
currentRunId?: string;
|
||||
requirements_met?: boolean;
|
||||
category?: string;
|
||||
icon?: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
requirements?: HandRequirement[];
|
||||
tools?: string[];
|
||||
metrics?: string[];
|
||||
toolCount?: number;
|
||||
metricCount?: number;
|
||||
}
|
||||
|
||||
export interface HandRun {
|
||||
runId: string;
|
||||
status: string;
|
||||
startedAt: string;
|
||||
completedAt?: string;
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface Trigger {
|
||||
id: string;
|
||||
type: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export type ApprovalStatus = 'pending' | 'approved' | 'rejected' | 'expired';
|
||||
|
||||
export interface Approval {
|
||||
id: string;
|
||||
handName: string;
|
||||
runId?: string;
|
||||
status: ApprovalStatus;
|
||||
requestedAt: string;
|
||||
requestedBy?: string;
|
||||
reason?: string;
|
||||
action?: string;
|
||||
params?: Record<string, unknown>;
|
||||
respondedAt?: string;
|
||||
respondedBy?: string;
|
||||
responseReason?: string;
|
||||
}
|
||||
|
||||
// === Raw API Response Types (for mapping) ===
|
||||
|
||||
interface RawHandRequirement {
|
||||
description?: string;
|
||||
name?: string;
|
||||
met?: boolean;
|
||||
satisfied?: boolean;
|
||||
details?: string;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
interface RawHandRun {
|
||||
runId?: string;
|
||||
run_id?: string;
|
||||
id?: string;
|
||||
status?: string;
|
||||
startedAt?: string;
|
||||
started_at?: string;
|
||||
created_at?: string;
|
||||
completedAt?: string;
|
||||
completed_at?: string;
|
||||
finished_at?: string;
|
||||
result?: unknown;
|
||||
output?: unknown;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface RawApproval {
|
||||
id?: string;
|
||||
approvalId?: string;
|
||||
approval_id?: string;
|
||||
type?: string;
|
||||
request_type?: string;
|
||||
handId?: string;
|
||||
hand_id?: string;
|
||||
hand_name?: string;
|
||||
handName?: string;
|
||||
run_id?: string;
|
||||
runId?: string;
|
||||
requester?: string;
|
||||
requested_by?: string;
|
||||
requestedAt?: string;
|
||||
requested_at?: string;
|
||||
status?: string;
|
||||
reason?: string;
|
||||
description?: string;
|
||||
action?: string;
|
||||
params?: Record<string, unknown>;
|
||||
responded_at?: string;
|
||||
respondedAt?: string;
|
||||
responded_by?: string;
|
||||
respondedBy?: string;
|
||||
response_reason?: string;
|
||||
responseReason?: string;
|
||||
}
|
||||
|
||||
// === Store Interface ===
|
||||
|
||||
interface HandClient {
|
||||
listHands: () => Promise<{ hands?: Array<Record<string, unknown>> } | null>;
|
||||
getHand: (name: string) => Promise<Record<string, unknown> | null>;
|
||||
listHandRuns: (name: string, opts?: { limit?: number; offset?: number }) => Promise<{ runs?: RawHandRun[] } | null>;
|
||||
triggerHand: (name: string, params?: Record<string, unknown>) => Promise<{ runId?: string; status?: string } | null>;
|
||||
approveHand: (name: string, runId: string, approved: boolean, reason?: string) => Promise<void>;
|
||||
cancelHand: (name: string, runId: string) => Promise<void>;
|
||||
listTriggers: () => Promise<{ triggers?: Trigger[] } | null>;
|
||||
getTrigger: (id: string) => Promise<Trigger | null>;
|
||||
createTrigger: (trigger: { type: string; name?: string; enabled?: boolean; config?: Record<string, unknown>; handName?: string; workflowId?: string }) => Promise<{ id?: string } | null>;
|
||||
updateTrigger: (id: string, updates: { name?: string; enabled?: boolean; config?: Record<string, unknown>; handName?: string; workflowId?: string }) => Promise<void>;
|
||||
deleteTrigger: (id: string) => Promise<void>;
|
||||
listApprovals: (status?: ApprovalStatus) => Promise<{ approvals?: RawApproval[] } | null>;
|
||||
respondToApproval: (approvalId: string, approved: boolean, reason?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
interface HandStore {
|
||||
// State
|
||||
hands: Hand[];
|
||||
handRuns: Record<string, HandRun[]>;
|
||||
triggers: Trigger[];
|
||||
approvals: Approval[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Client reference (injected via setHandStoreClient)
|
||||
client: HandClient | null;
|
||||
|
||||
// Actions
|
||||
setHandStoreClient: (client: HandClient) => void;
|
||||
loadHands: () => Promise<void>;
|
||||
getHandDetails: (name: string) => Promise<Hand | undefined>;
|
||||
triggerHand: (name: string, params?: Record<string, unknown>) => Promise<HandRun | undefined>;
|
||||
loadHandRuns: (name: string, opts?: { limit?: number; offset?: number }) => Promise<HandRun[]>;
|
||||
approveHand: (name: string, runId: string, approved: boolean, reason?: string) => Promise<void>;
|
||||
cancelHand: (name: string, runId: string) => Promise<void>;
|
||||
loadTriggers: () => Promise<void>;
|
||||
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>;
|
||||
loadApprovals: (status?: ApprovalStatus) => Promise<void>;
|
||||
respondToApproval: (approvalId: string, approved: boolean, reason?: string) => Promise<void>;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const useHandStore = create<HandStore>((set, get) => ({
|
||||
// Initial State
|
||||
hands: [],
|
||||
handRuns: {},
|
||||
triggers: [],
|
||||
approvals: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
client: null,
|
||||
|
||||
// Client injection
|
||||
setHandStoreClient: (client: HandClient) => {
|
||||
set({ client });
|
||||
},
|
||||
|
||||
// === Hand Actions ===
|
||||
|
||||
loadHands: async () => {
|
||||
const client = get().client;
|
||||
if (!client) return;
|
||||
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const result = await client.listHands();
|
||||
const validStatuses = ['idle', 'running', 'needs_approval', 'error', 'unavailable', 'setup_needed'] as const;
|
||||
const hands: Hand[] = (result?.hands || []).map((h: Record<string, unknown>) => {
|
||||
const status = validStatuses.includes(h.status as Hand['status'])
|
||||
? h.status as Hand['status']
|
||||
: (h.requirements_met ? 'idle' : 'setup_needed');
|
||||
return {
|
||||
id: String(h.id || h.name),
|
||||
name: String(h.name || ''),
|
||||
description: String(h.description || ''),
|
||||
status,
|
||||
requirements_met: Boolean(h.requirements_met),
|
||||
category: h.category as string | undefined,
|
||||
icon: h.icon as string | undefined,
|
||||
toolCount: (h.tool_count as number) || ((h.tools as unknown[])?.length),
|
||||
metricCount: (h.metric_count as number) || ((h.metrics as unknown[])?.length),
|
||||
};
|
||||
});
|
||||
set({ hands, isLoading: false });
|
||||
} catch {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
getHandDetails: async (name: string) => {
|
||||
const client = get().client;
|
||||
if (!client) return undefined;
|
||||
|
||||
try {
|
||||
const result = await client.getHand(name);
|
||||
if (!result) return undefined;
|
||||
|
||||
const getStringFromConfig = (key: string): string | undefined => {
|
||||
const val = (result.config as Record<string, unknown>)?.[key];
|
||||
return typeof val === 'string' ? val : undefined;
|
||||
};
|
||||
const getArrayFromConfig = (key: string): string[] | undefined => {
|
||||
const val = (result.config as Record<string, unknown>)?.[key];
|
||||
return Array.isArray(val) ? val : undefined;
|
||||
};
|
||||
|
||||
const validStatuses = ['idle', 'running', 'needs_approval', 'error', 'unavailable', 'setup_needed'] as const;
|
||||
const status = validStatuses.includes(result.status as Hand['status'])
|
||||
? result.status as Hand['status']
|
||||
: (result.requirements_met ? 'idle' : 'setup_needed');
|
||||
|
||||
const hand: Hand = {
|
||||
id: String(result.id || result.name || name),
|
||||
name: String(result.name || name),
|
||||
description: String(result.description || ''),
|
||||
status,
|
||||
requirements_met: Boolean(result.requirements_met),
|
||||
category: result.category as string | undefined,
|
||||
icon: result.icon as string | undefined,
|
||||
provider: (result.provider as string) || getStringFromConfig('provider'),
|
||||
model: (result.model as string) || getStringFromConfig('model'),
|
||||
requirements: ((result.requirements as RawHandRequirement[]) || []).map((r) => ({
|
||||
description: r.description || r.name || String(r),
|
||||
met: r.met ?? r.satisfied ?? true,
|
||||
details: r.details || r.hint,
|
||||
})),
|
||||
tools: (result.tools as string[]) || getArrayFromConfig('tools'),
|
||||
metrics: (result.metrics as string[]) || getArrayFromConfig('metrics'),
|
||||
toolCount: (result.tool_count as number) || ((result.tools as unknown[])?.length) || 0,
|
||||
metricCount: (result.metric_count as number) || ((result.metrics as unknown[])?.length) || 0,
|
||||
};
|
||||
|
||||
set(state => ({
|
||||
hands: state.hands.map(h => h.name === name ? { ...h, ...hand } : h),
|
||||
}));
|
||||
|
||||
return hand;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
loadHandRuns: async (name: string, opts?: { limit?: number; offset?: number }) => {
|
||||
const client = get().client;
|
||||
if (!client) return [];
|
||||
|
||||
try {
|
||||
const result = await client.listHandRuns(name, opts);
|
||||
const runs: HandRun[] = (result?.runs || []).map((r: RawHandRun) => ({
|
||||
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,
|
||||
}));
|
||||
set(state => ({
|
||||
handRuns: { ...state.handRuns, [name]: runs },
|
||||
}));
|
||||
return runs;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
triggerHand: async (name: string, params?: Record<string, unknown>) => {
|
||||
const client = get().client;
|
||||
if (!client) return undefined;
|
||||
|
||||
try {
|
||||
const result = await client.triggerHand(name, params);
|
||||
if (!result) return undefined;
|
||||
|
||||
const run: HandRun = {
|
||||
runId: result.runId || '',
|
||||
status: result.status || 'running',
|
||||
startedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Add run to local state
|
||||
set(state => ({
|
||||
handRuns: {
|
||||
...state.handRuns,
|
||||
[name]: [run, ...(state.handRuns[name] || [])],
|
||||
},
|
||||
}));
|
||||
|
||||
// Refresh hands to update status
|
||||
await get().loadHands();
|
||||
|
||||
return run;
|
||||
} catch (err: unknown) {
|
||||
set({ error: err instanceof Error ? err.message : String(err) });
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
approveHand: async (name: string, runId: string, approved: boolean, reason?: string) => {
|
||||
const client = get().client;
|
||||
if (!client) return;
|
||||
|
||||
try {
|
||||
await client.approveHand(name, runId, approved, reason);
|
||||
await get().loadHands();
|
||||
} catch (err: unknown) {
|
||||
set({ error: err instanceof Error ? err.message : String(err) });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
cancelHand: async (name: string, runId: string) => {
|
||||
const client = get().client;
|
||||
if (!client) return;
|
||||
|
||||
try {
|
||||
await client.cancelHand(name, runId);
|
||||
await get().loadHands();
|
||||
} catch (err: unknown) {
|
||||
set({ error: err instanceof Error ? err.message : String(err) });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// === Trigger Actions ===
|
||||
|
||||
loadTriggers: async () => {
|
||||
const client = get().client;
|
||||
if (!client) return;
|
||||
|
||||
try {
|
||||
const result = await client.listTriggers();
|
||||
set({ triggers: result?.triggers || [] });
|
||||
} catch {
|
||||
// ignore if triggers API not available
|
||||
}
|
||||
},
|
||||
|
||||
getTrigger: async (id: string) => {
|
||||
const client = get().client;
|
||||
if (!client) return undefined;
|
||||
|
||||
try {
|
||||
const result = await client.getTrigger(id);
|
||||
if (!result) return undefined;
|
||||
return {
|
||||
id: result.id,
|
||||
type: result.type,
|
||||
enabled: result.enabled,
|
||||
} as Trigger;
|
||||
} catch (err: unknown) {
|
||||
set({ error: err instanceof Error ? err.message : String(err) });
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
createTrigger: async (trigger) => {
|
||||
const client = get().client;
|
||||
if (!client) return undefined;
|
||||
|
||||
try {
|
||||
const result = await client.createTrigger(trigger);
|
||||
if (!result?.id) return undefined;
|
||||
await get().loadTriggers();
|
||||
return get().triggers.find(t => t.id === result.id);
|
||||
} catch (err: unknown) {
|
||||
set({ error: err instanceof Error ? err.message : String(err) });
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
updateTrigger: async (id: string, updates) => {
|
||||
const client = get().client;
|
||||
if (!client) return undefined;
|
||||
|
||||
try {
|
||||
await client.updateTrigger(id, updates);
|
||||
set(state => ({
|
||||
triggers: state.triggers.map(t =>
|
||||
t.id === id ? { ...t, ...updates } : t
|
||||
),
|
||||
}));
|
||||
return get().triggers.find(t => t.id === id);
|
||||
} catch (err: unknown) {
|
||||
set({ error: err instanceof Error ? err.message : String(err) });
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
deleteTrigger: async (id: string) => {
|
||||
const client = get().client;
|
||||
if (!client) return;
|
||||
|
||||
try {
|
||||
await client.deleteTrigger(id);
|
||||
set(state => ({
|
||||
triggers: state.triggers.filter(t => t.id !== id),
|
||||
}));
|
||||
} catch (err: unknown) {
|
||||
set({ error: err instanceof Error ? err.message : String(err) });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// === Approval Actions ===
|
||||
|
||||
loadApprovals: async (status?: ApprovalStatus) => {
|
||||
const client = get().client;
|
||||
if (!client) return;
|
||||
|
||||
try {
|
||||
const result = await client.listApprovals(status);
|
||||
const approvals: Approval[] = (result?.approvals || []).map((a: RawApproval) => ({
|
||||
id: a.id || a.approval_id || a.approvalId || '',
|
||||
handName: a.hand_name || a.handName || '',
|
||||
runId: a.run_id || a.runId,
|
||||
status: (a.status || 'pending') as ApprovalStatus,
|
||||
requestedAt: a.requested_at || a.requestedAt || new Date().toISOString(),
|
||||
requestedBy: a.requested_by || a.requester,
|
||||
reason: a.reason || a.description,
|
||||
action: a.action || 'execute',
|
||||
params: a.params,
|
||||
respondedAt: a.responded_at || a.respondedAt,
|
||||
respondedBy: a.responded_by || a.respondedBy,
|
||||
responseReason: a.response_reason || a.responseReason,
|
||||
}));
|
||||
set({ approvals });
|
||||
} catch {
|
||||
// ignore if approvals API not available
|
||||
}
|
||||
},
|
||||
|
||||
respondToApproval: async (approvalId: string, approved: boolean, reason?: string) => {
|
||||
const client = get().client;
|
||||
if (!client) return;
|
||||
|
||||
try {
|
||||
await client.respondToApproval(approvalId, approved, reason);
|
||||
await get().loadApprovals();
|
||||
} catch (err: unknown) {
|
||||
set({ error: err instanceof Error ? err.message : String(err) });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
clearError: () => set({ error: null }),
|
||||
}));
|
||||
|
||||
/**
|
||||
* Helper to create a HandClient adapter from a GatewayClient.
|
||||
* Use this to inject the client into handStore.
|
||||
*/
|
||||
export function createHandClientFromGateway(client: GatewayClient): HandClient {
|
||||
return {
|
||||
listHands: () => client.listHands(),
|
||||
getHand: (name) => client.getHand(name),
|
||||
listHandRuns: (name, opts) => client.listHandRuns(name, opts),
|
||||
triggerHand: (name, params) => client.triggerHand(name, params),
|
||||
approveHand: (name, runId, approved, reason) => client.approveHand(name, runId, approved, reason),
|
||||
cancelHand: (name, runId) => client.cancelHand(name, runId),
|
||||
listTriggers: () => client.listTriggers(),
|
||||
getTrigger: (id) => client.getTrigger(id),
|
||||
createTrigger: (trigger) => client.createTrigger(trigger),
|
||||
updateTrigger: (id, updates) => client.updateTrigger(id, updates),
|
||||
deleteTrigger: (id) => client.deleteTrigger(id),
|
||||
listApprovals: (status) => client.listApprovals(status),
|
||||
respondToApproval: (approvalId, approved, reason) => client.respondToApproval(approvalId, approved, reason),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user