First Release of Claw3D (#11)
Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,585 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useReducer,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import {
|
||||
areTranscriptEntriesEqual,
|
||||
buildOutputLinesFromTranscriptEntries,
|
||||
buildTranscriptEntriesFromLines,
|
||||
createTranscriptEntryFromLine,
|
||||
sortTranscriptEntries,
|
||||
TRANSCRIPT_V2_ENABLED,
|
||||
type TranscriptAppendMeta,
|
||||
type TranscriptEntry,
|
||||
} from "@/features/agents/state/transcript";
|
||||
|
||||
export type AgentStatus = "idle" | "running" | "error";
|
||||
export type FocusFilter = "all" | "running" | "approvals";
|
||||
|
||||
export type AgentStoreSeed = {
|
||||
agentId: string;
|
||||
name: string;
|
||||
sessionKey: string;
|
||||
avatarSeed?: string | null;
|
||||
avatarUrl?: string | null;
|
||||
model?: string | null;
|
||||
thinkingLevel?: string | null;
|
||||
sessionExecHost?: "sandbox" | "gateway" | "node";
|
||||
sessionExecSecurity?: "deny" | "allowlist" | "full";
|
||||
sessionExecAsk?: "off" | "on-miss" | "always";
|
||||
toolCallingEnabled?: boolean;
|
||||
showThinkingTraces?: boolean;
|
||||
};
|
||||
|
||||
export type AgentState = AgentStoreSeed & {
|
||||
status: AgentStatus;
|
||||
sessionCreated: boolean;
|
||||
awaitingUserInput: boolean;
|
||||
hasUnseenActivity: boolean;
|
||||
outputLines: string[];
|
||||
lastResult: string | null;
|
||||
lastDiff: string | null;
|
||||
runId: string | null;
|
||||
runStartedAt: number | null;
|
||||
streamText: string | null;
|
||||
thinkingTrace: string | null;
|
||||
latestOverride: string | null;
|
||||
latestOverrideKind: "heartbeat" | "cron" | null;
|
||||
lastAssistantMessageAt: number | null;
|
||||
lastActivityAt: number | null;
|
||||
latestPreview: string | null;
|
||||
lastUserMessage: string | null;
|
||||
draft: string;
|
||||
queuedMessages?: string[];
|
||||
sessionSettingsSynced: boolean;
|
||||
historyLoadedAt: number | null;
|
||||
historyFetchLimit: number | null;
|
||||
historyFetchedCount: number | null;
|
||||
historyMaybeTruncated: boolean;
|
||||
toolCallingEnabled: boolean;
|
||||
showThinkingTraces: boolean;
|
||||
transcriptEntries?: TranscriptEntry[];
|
||||
transcriptRevision?: number;
|
||||
transcriptSequenceCounter?: number;
|
||||
sessionEpoch?: number;
|
||||
lastHistoryRequestRevision?: number | null;
|
||||
lastAppliedHistoryRequestId?: string | null;
|
||||
};
|
||||
|
||||
export const buildNewSessionAgentPatch = (agent: AgentState): Partial<AgentState> => {
|
||||
return {
|
||||
sessionKey: agent.sessionKey,
|
||||
status: "idle",
|
||||
runId: null,
|
||||
runStartedAt: null,
|
||||
streamText: null,
|
||||
thinkingTrace: null,
|
||||
outputLines: [],
|
||||
lastResult: null,
|
||||
lastDiff: null,
|
||||
latestOverride: null,
|
||||
latestOverrideKind: null,
|
||||
lastAssistantMessageAt: null,
|
||||
lastActivityAt: null,
|
||||
latestPreview: null,
|
||||
lastUserMessage: null,
|
||||
draft: "",
|
||||
queuedMessages: [],
|
||||
historyLoadedAt: null,
|
||||
historyFetchLimit: null,
|
||||
historyFetchedCount: null,
|
||||
historyMaybeTruncated: false,
|
||||
awaitingUserInput: false,
|
||||
hasUnseenActivity: false,
|
||||
sessionCreated: true,
|
||||
sessionSettingsSynced: true,
|
||||
transcriptEntries: [],
|
||||
transcriptRevision: (agent.transcriptRevision ?? 0) + 1,
|
||||
transcriptSequenceCounter: 0,
|
||||
sessionEpoch: (agent.sessionEpoch ?? 0) + 1,
|
||||
lastHistoryRequestRevision: null,
|
||||
lastAppliedHistoryRequestId: null,
|
||||
};
|
||||
};
|
||||
|
||||
export type AgentStoreState = {
|
||||
agents: AgentState[];
|
||||
selectedAgentId: string | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
type Action =
|
||||
| { type: "hydrateAgents"; agents: AgentStoreSeed[]; selectedAgentId?: string }
|
||||
| { type: "setError"; error: string | null }
|
||||
| { type: "setLoading"; loading: boolean }
|
||||
| { type: "updateAgent"; agentId: string; patch: Partial<AgentState> }
|
||||
| { type: "appendOutput"; agentId: string; line: string; transcript?: TranscriptAppendMeta }
|
||||
| { type: "enqueueQueuedMessage"; agentId: string; message: string }
|
||||
| { type: "removeQueuedMessage"; agentId: string; index: number }
|
||||
| { type: "shiftQueuedMessage"; agentId: string; expectedMessage?: string }
|
||||
| { type: "markActivity"; agentId: string; at?: number }
|
||||
| { type: "selectAgent"; agentId: string | null };
|
||||
|
||||
const initialState: AgentStoreState = {
|
||||
agents: [],
|
||||
selectedAgentId: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
const areStringArraysEqual = (left: string[], right: string[]): boolean => {
|
||||
if (left.length !== right.length) return false;
|
||||
for (let i = 0; i < left.length; i += 1) {
|
||||
if (left[i] !== right[i]) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const ensureTranscriptEntries = (agent: AgentState): TranscriptEntry[] => {
|
||||
if (Array.isArray(agent.transcriptEntries)) {
|
||||
return agent.transcriptEntries;
|
||||
}
|
||||
return buildTranscriptEntriesFromLines({
|
||||
lines: agent.outputLines,
|
||||
sessionKey: agent.sessionKey,
|
||||
source: "legacy",
|
||||
startSequence: 0,
|
||||
confirmed: true,
|
||||
});
|
||||
};
|
||||
|
||||
const nextTranscriptSequenceCounter = (
|
||||
currentCounter: number | undefined,
|
||||
entries: TranscriptEntry[]
|
||||
): number => {
|
||||
const derived = entries.reduce((max, entry) => Math.max(max, entry.sequenceKey + 1), 0);
|
||||
return Math.max(currentCounter ?? 0, derived);
|
||||
};
|
||||
|
||||
const createRuntimeAgentState = (
|
||||
seed: AgentStoreSeed,
|
||||
existing?: AgentState | null
|
||||
): AgentState => {
|
||||
const sameSessionKey = existing?.sessionKey === seed.sessionKey;
|
||||
const outputLines = sameSessionKey ? (existing?.outputLines ?? []) : [];
|
||||
const queuedMessages = sameSessionKey ? [...(existing?.queuedMessages ?? [])] : [];
|
||||
const transcriptEntries = sameSessionKey
|
||||
? Array.isArray(existing?.transcriptEntries)
|
||||
? existing.transcriptEntries
|
||||
: buildTranscriptEntriesFromLines({
|
||||
lines: outputLines,
|
||||
sessionKey: seed.sessionKey,
|
||||
source: "legacy",
|
||||
startSequence: 0,
|
||||
confirmed: true,
|
||||
})
|
||||
: [];
|
||||
return {
|
||||
...seed,
|
||||
avatarSeed: seed.avatarSeed ?? existing?.avatarSeed ?? seed.agentId,
|
||||
avatarUrl: seed.avatarUrl ?? existing?.avatarUrl ?? null,
|
||||
model: seed.model ?? existing?.model ?? null,
|
||||
thinkingLevel: seed.thinkingLevel ?? existing?.thinkingLevel ?? "high",
|
||||
sessionExecHost: seed.sessionExecHost ?? existing?.sessionExecHost,
|
||||
sessionExecSecurity: seed.sessionExecSecurity ?? existing?.sessionExecSecurity,
|
||||
sessionExecAsk: seed.sessionExecAsk ?? existing?.sessionExecAsk,
|
||||
status: sameSessionKey ? (existing?.status ?? "idle") : "idle",
|
||||
sessionCreated: sameSessionKey ? (existing?.sessionCreated ?? false) : false,
|
||||
awaitingUserInput: sameSessionKey ? (existing?.awaitingUserInput ?? false) : false,
|
||||
hasUnseenActivity: sameSessionKey ? (existing?.hasUnseenActivity ?? false) : false,
|
||||
outputLines,
|
||||
lastResult: sameSessionKey ? (existing?.lastResult ?? null) : null,
|
||||
lastDiff: sameSessionKey ? (existing?.lastDiff ?? null) : null,
|
||||
runId: sameSessionKey ? (existing?.runId ?? null) : null,
|
||||
runStartedAt: sameSessionKey ? (existing?.runStartedAt ?? null) : null,
|
||||
streamText: sameSessionKey ? (existing?.streamText ?? null) : null,
|
||||
thinkingTrace: sameSessionKey ? (existing?.thinkingTrace ?? null) : null,
|
||||
latestOverride: sameSessionKey ? (existing?.latestOverride ?? null) : null,
|
||||
latestOverrideKind: sameSessionKey ? (existing?.latestOverrideKind ?? null) : null,
|
||||
lastAssistantMessageAt: sameSessionKey ? (existing?.lastAssistantMessageAt ?? null) : null,
|
||||
lastActivityAt: sameSessionKey ? (existing?.lastActivityAt ?? null) : null,
|
||||
latestPreview: sameSessionKey ? (existing?.latestPreview ?? null) : null,
|
||||
lastUserMessage: sameSessionKey ? (existing?.lastUserMessage ?? null) : null,
|
||||
draft: sameSessionKey ? (existing?.draft ?? "") : "",
|
||||
queuedMessages,
|
||||
sessionSettingsSynced: sameSessionKey ? (existing?.sessionSettingsSynced ?? false) : false,
|
||||
historyLoadedAt: sameSessionKey ? (existing?.historyLoadedAt ?? null) : null,
|
||||
historyFetchLimit: sameSessionKey ? (existing?.historyFetchLimit ?? null) : null,
|
||||
historyFetchedCount: sameSessionKey ? (existing?.historyFetchedCount ?? null) : null,
|
||||
historyMaybeTruncated: sameSessionKey ? (existing?.historyMaybeTruncated ?? false) : false,
|
||||
toolCallingEnabled: seed.toolCallingEnabled ?? existing?.toolCallingEnabled ?? false,
|
||||
showThinkingTraces: seed.showThinkingTraces ?? existing?.showThinkingTraces ?? true,
|
||||
transcriptEntries,
|
||||
transcriptRevision: sameSessionKey
|
||||
? (existing?.transcriptRevision ?? outputLines.length)
|
||||
: 0,
|
||||
transcriptSequenceCounter: sameSessionKey
|
||||
? (existing?.transcriptSequenceCounter ??
|
||||
nextTranscriptSequenceCounter(existing?.transcriptSequenceCounter, transcriptEntries))
|
||||
: 0,
|
||||
sessionEpoch: sameSessionKey
|
||||
? (existing?.sessionEpoch ?? 0)
|
||||
: (existing?.sessionEpoch ?? 0) + 1,
|
||||
lastHistoryRequestRevision: sameSessionKey
|
||||
? (existing?.lastHistoryRequestRevision ?? null)
|
||||
: null,
|
||||
lastAppliedHistoryRequestId: sameSessionKey
|
||||
? (existing?.lastAppliedHistoryRequestId ?? null)
|
||||
: null,
|
||||
};
|
||||
};
|
||||
|
||||
const reducer = (state: AgentStoreState, action: Action): AgentStoreState => {
|
||||
switch (action.type) {
|
||||
case "hydrateAgents": {
|
||||
const byId = new Map(state.agents.map((agent) => [agent.agentId, agent]));
|
||||
const agents = action.agents.map((seed) =>
|
||||
createRuntimeAgentState(seed, byId.get(seed.agentId))
|
||||
);
|
||||
const requestedSelectedAgentId = action.selectedAgentId?.trim() ?? "";
|
||||
const selectedAgentId =
|
||||
requestedSelectedAgentId &&
|
||||
agents.some((agent) => agent.agentId === requestedSelectedAgentId)
|
||||
? requestedSelectedAgentId
|
||||
: state.selectedAgentId &&
|
||||
agents.some((agent) => agent.agentId === state.selectedAgentId)
|
||||
? state.selectedAgentId
|
||||
: agents[0]?.agentId ?? null;
|
||||
return {
|
||||
...state,
|
||||
agents,
|
||||
selectedAgentId,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
case "setError":
|
||||
return { ...state, error: action.error, loading: false };
|
||||
case "setLoading":
|
||||
return { ...state, loading: action.loading };
|
||||
case "updateAgent":
|
||||
return {
|
||||
...state,
|
||||
agents: state.agents.map((agent) => {
|
||||
if (agent.agentId !== action.agentId) return agent;
|
||||
const patch = action.patch;
|
||||
const nextSessionKey = (patch.sessionKey ?? agent.sessionKey).trim();
|
||||
const sessionKeyChanged = nextSessionKey !== agent.sessionKey.trim();
|
||||
const patchHasTranscriptEntries = Array.isArray(patch.transcriptEntries);
|
||||
const patchHasOutputLines = Array.isArray(patch.outputLines);
|
||||
const patchMutatesTranscript = patchHasTranscriptEntries || patchHasOutputLines;
|
||||
|
||||
const existingEntries = ensureTranscriptEntries(agent);
|
||||
const base: AgentState = { ...agent, ...patch };
|
||||
let nextEntries: TranscriptEntry[] = existingEntries;
|
||||
if (Array.isArray(base.transcriptEntries)) {
|
||||
nextEntries = base.transcriptEntries as TranscriptEntry[];
|
||||
}
|
||||
let nextOutputLines: string[] = agent.outputLines;
|
||||
if (Array.isArray(base.outputLines)) {
|
||||
nextOutputLines = base.outputLines as string[];
|
||||
}
|
||||
let transcriptMutated = false;
|
||||
|
||||
if (patchHasTranscriptEntries) {
|
||||
const patchedTranscriptEntries = patch.transcriptEntries as TranscriptEntry[];
|
||||
const normalized = TRANSCRIPT_V2_ENABLED
|
||||
? sortTranscriptEntries(patchedTranscriptEntries)
|
||||
: [...patchedTranscriptEntries];
|
||||
transcriptMutated = !areTranscriptEntriesEqual(existingEntries, normalized);
|
||||
nextEntries = normalized;
|
||||
nextOutputLines = buildOutputLinesFromTranscriptEntries(normalized);
|
||||
} else if (patchHasOutputLines) {
|
||||
const patchedOutputLines = patch.outputLines as string[];
|
||||
const rebuilt = buildTranscriptEntriesFromLines({
|
||||
lines: patchedOutputLines,
|
||||
sessionKey: nextSessionKey || agent.sessionKey,
|
||||
source: "legacy",
|
||||
startSequence: 0,
|
||||
confirmed: true,
|
||||
});
|
||||
const normalized = TRANSCRIPT_V2_ENABLED ? sortTranscriptEntries(rebuilt) : rebuilt;
|
||||
transcriptMutated = !areStringArraysEqual(agent.outputLines, patchedOutputLines);
|
||||
nextEntries = normalized;
|
||||
nextOutputLines = TRANSCRIPT_V2_ENABLED
|
||||
? buildOutputLinesFromTranscriptEntries(normalized)
|
||||
: [...patchedOutputLines];
|
||||
}
|
||||
|
||||
const revision = transcriptMutated
|
||||
? (agent.transcriptRevision ?? 0) + 1
|
||||
: (patch.transcriptRevision ?? agent.transcriptRevision ?? 0);
|
||||
const nextCounter = patchMutatesTranscript
|
||||
? nextTranscriptSequenceCounter(base.transcriptSequenceCounter, nextEntries)
|
||||
: (base.transcriptSequenceCounter ?? agent.transcriptSequenceCounter ?? 0);
|
||||
|
||||
return {
|
||||
...base,
|
||||
outputLines: nextOutputLines,
|
||||
transcriptEntries: nextEntries,
|
||||
transcriptRevision: revision,
|
||||
transcriptSequenceCounter: nextCounter,
|
||||
sessionEpoch:
|
||||
patch.sessionEpoch !== undefined
|
||||
? patch.sessionEpoch
|
||||
: sessionKeyChanged
|
||||
? (agent.sessionEpoch ?? 0) + 1
|
||||
: (agent.sessionEpoch ?? 0),
|
||||
};
|
||||
}),
|
||||
};
|
||||
case "appendOutput":
|
||||
return {
|
||||
...state,
|
||||
agents: state.agents.map((agent) => {
|
||||
if (agent.agentId !== action.agentId) return agent;
|
||||
const existingEntries = ensureTranscriptEntries(agent);
|
||||
const nextSequence = nextTranscriptSequenceCounter(
|
||||
agent.transcriptSequenceCounter,
|
||||
existingEntries
|
||||
);
|
||||
const nextEntry = createTranscriptEntryFromLine({
|
||||
line: action.line,
|
||||
sessionKey: action.transcript?.sessionKey ?? agent.sessionKey,
|
||||
source: action.transcript?.source ?? "legacy",
|
||||
runId: action.transcript?.runId ?? agent.runId,
|
||||
timestampMs: action.transcript?.timestampMs,
|
||||
fallbackTimestampMs: action.transcript?.timestampMs ?? Date.now(),
|
||||
role: action.transcript?.role,
|
||||
kind: action.transcript?.kind,
|
||||
entryId: action.transcript?.entryId,
|
||||
confirmed: action.transcript?.confirmed,
|
||||
sequenceKey: nextSequence,
|
||||
});
|
||||
if (!nextEntry) {
|
||||
return { ...agent, outputLines: [...agent.outputLines, action.line] };
|
||||
}
|
||||
const nextEntryId = nextEntry.entryId.trim();
|
||||
const existingIndex =
|
||||
nextEntryId.length > 0
|
||||
? existingEntries.findIndex((entry) => entry.entryId === nextEntryId)
|
||||
: -1;
|
||||
const hasReplacement = existingIndex >= 0;
|
||||
|
||||
let nextEntries: TranscriptEntry[];
|
||||
if (hasReplacement) {
|
||||
let replacedOne = false;
|
||||
const replaced = existingEntries.reduce<TranscriptEntry[]>((acc, entry) => {
|
||||
if (entry.entryId !== nextEntryId) {
|
||||
acc.push(entry);
|
||||
return acc;
|
||||
}
|
||||
if (replacedOne) {
|
||||
return acc;
|
||||
}
|
||||
replacedOne = true;
|
||||
acc.push({
|
||||
...nextEntry,
|
||||
sequenceKey: entry.sequenceKey,
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
nextEntries = TRANSCRIPT_V2_ENABLED ? sortTranscriptEntries(replaced) : replaced;
|
||||
} else {
|
||||
const appended = [...existingEntries, nextEntry];
|
||||
nextEntries = TRANSCRIPT_V2_ENABLED ? sortTranscriptEntries(appended) : appended;
|
||||
}
|
||||
|
||||
return {
|
||||
...agent,
|
||||
outputLines:
|
||||
TRANSCRIPT_V2_ENABLED || hasReplacement
|
||||
? buildOutputLinesFromTranscriptEntries(nextEntries)
|
||||
: [...agent.outputLines, action.line],
|
||||
transcriptEntries: nextEntries,
|
||||
transcriptRevision: (agent.transcriptRevision ?? 0) + 1,
|
||||
transcriptSequenceCounter: Math.max(
|
||||
agent.transcriptSequenceCounter ?? 0,
|
||||
nextEntry.sequenceKey + 1
|
||||
),
|
||||
};
|
||||
}),
|
||||
};
|
||||
case "enqueueQueuedMessage":
|
||||
return {
|
||||
...state,
|
||||
agents: state.agents.map((agent) => {
|
||||
if (agent.agentId !== action.agentId) return agent;
|
||||
const message = action.message.trim();
|
||||
if (!message) return agent;
|
||||
const queuedMessages = [...(agent.queuedMessages ?? []), message];
|
||||
return { ...agent, queuedMessages };
|
||||
}),
|
||||
};
|
||||
case "removeQueuedMessage":
|
||||
return {
|
||||
...state,
|
||||
agents: state.agents.map((agent) => {
|
||||
if (agent.agentId !== action.agentId) return agent;
|
||||
if (!Number.isInteger(action.index) || action.index < 0) return agent;
|
||||
const queuedMessages = agent.queuedMessages ?? [];
|
||||
if (action.index >= queuedMessages.length) return agent;
|
||||
return {
|
||||
...agent,
|
||||
queuedMessages: queuedMessages.filter((_, index) => index !== action.index),
|
||||
};
|
||||
}),
|
||||
};
|
||||
case "shiftQueuedMessage":
|
||||
return {
|
||||
...state,
|
||||
agents: state.agents.map((agent) => {
|
||||
if (agent.agentId !== action.agentId) return agent;
|
||||
const queuedMessages = agent.queuedMessages ?? [];
|
||||
if (queuedMessages.length === 0) return agent;
|
||||
if (
|
||||
action.expectedMessage !== undefined &&
|
||||
action.expectedMessage.trim() !== queuedMessages[0]
|
||||
) {
|
||||
return agent;
|
||||
}
|
||||
return { ...agent, queuedMessages: queuedMessages.slice(1) };
|
||||
}),
|
||||
};
|
||||
case "markActivity": {
|
||||
const at = action.at ?? Date.now();
|
||||
return {
|
||||
...state,
|
||||
agents: state.agents.map((agent) => {
|
||||
if (agent.agentId !== action.agentId) return agent;
|
||||
const isSelected = state.selectedAgentId === action.agentId;
|
||||
return {
|
||||
...agent,
|
||||
lastActivityAt: at,
|
||||
hasUnseenActivity: isSelected ? false : true,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
case "selectAgent": {
|
||||
if (action.agentId === state.selectedAgentId) {
|
||||
if (action.agentId === null) {
|
||||
return state;
|
||||
}
|
||||
const selected = state.agents.find((agent) => agent.agentId === action.agentId) ?? null;
|
||||
if (!selected || !selected.hasUnseenActivity) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
selectedAgentId: action.agentId,
|
||||
agents:
|
||||
action.agentId === null
|
||||
? state.agents
|
||||
: state.agents.map((agent) =>
|
||||
agent.agentId === action.agentId
|
||||
? { ...agent, hasUnseenActivity: false }
|
||||
: agent
|
||||
),
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const agentStoreReducer = reducer;
|
||||
export const initialAgentStoreState = initialState;
|
||||
|
||||
type AgentStoreContextValue = {
|
||||
state: AgentStoreState;
|
||||
dispatch: React.Dispatch<Action>;
|
||||
hydrateAgents: (agents: AgentStoreSeed[], selectedAgentId?: string) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
};
|
||||
|
||||
const AgentStoreContext = createContext<AgentStoreContextValue | null>(null);
|
||||
|
||||
export const AgentStoreProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
const hydrateAgents = useCallback(
|
||||
(agents: AgentStoreSeed[], selectedAgentId?: string) => {
|
||||
dispatch({ type: "hydrateAgents", agents, selectedAgentId });
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setLoading = useCallback(
|
||||
(loading: boolean) => dispatch({ type: "setLoading", loading }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setError = useCallback(
|
||||
(error: string | null) => dispatch({ type: "setError", error }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ state, dispatch, hydrateAgents, setLoading, setError }),
|
||||
[dispatch, hydrateAgents, setError, setLoading, state]
|
||||
);
|
||||
|
||||
return (
|
||||
<AgentStoreContext.Provider value={value}>{children}</AgentStoreContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAgentStore = () => {
|
||||
const ctx = useContext(AgentStoreContext);
|
||||
if (!ctx) {
|
||||
throw new Error("AgentStoreProvider is missing.");
|
||||
}
|
||||
return ctx;
|
||||
};
|
||||
|
||||
export const getSelectedAgent = (state: AgentStoreState): AgentState | null => {
|
||||
if (!state.selectedAgentId) return null;
|
||||
return state.agents.find((agent) => agent.agentId === state.selectedAgentId) ?? null;
|
||||
};
|
||||
|
||||
export const getFilteredAgents = (state: AgentStoreState, filter: FocusFilter): AgentState[] => {
|
||||
const statusPriority: Record<AgentStatus, number> = {
|
||||
running: 0,
|
||||
idle: 1,
|
||||
error: 2,
|
||||
};
|
||||
const getActivityTimestamp = (agent: AgentState) =>
|
||||
Math.max(agent.lastActivityAt ?? 0, agent.runStartedAt ?? 0, agent.lastAssistantMessageAt ?? 0);
|
||||
const sortAgents = (agents: AgentState[], prioritizeStatus: boolean) =>
|
||||
agents
|
||||
.map((agent, index) => ({ agent, index }))
|
||||
.sort((left, right) => {
|
||||
if (prioritizeStatus) {
|
||||
const statusDelta =
|
||||
statusPriority[left.agent.status] - statusPriority[right.agent.status];
|
||||
if (statusDelta !== 0) return statusDelta;
|
||||
}
|
||||
const timeDelta = getActivityTimestamp(right.agent) - getActivityTimestamp(left.agent);
|
||||
if (timeDelta !== 0) return timeDelta;
|
||||
return left.index - right.index;
|
||||
})
|
||||
.map(({ agent }) => agent);
|
||||
switch (filter) {
|
||||
case "all":
|
||||
return sortAgents(state.agents, true);
|
||||
case "running":
|
||||
return sortAgents(state.agents.filter((agent) => agent.status === "running"), false);
|
||||
case "approvals":
|
||||
return sortAgents(state.agents.filter((agent) => agent.awaitingUserInput), false);
|
||||
default: {
|
||||
const _exhaustive: never = filter;
|
||||
void _exhaustive;
|
||||
return sortAgents(state.agents, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user