refactor(web): extract messaging status, agent skills and mention target hooks
Behaviour-preserving move of ShellPage data loaders into named hooks: - useMessagingStatus: the messaging surface flag and providers, loaded once a session user exists. - useAgentSkills: the agent skill catalog for slash commands, its refresh, and the setter bot settings use after editing skills. - useMentionTargets: composer mention options built from bots, group members, routines and connectors, including the routine/connector loader. Each hook is called where its effect was, so effect order is unchanged, and the hooks take the same objects the effects read so bodies and dependency arrays move verbatim. The mention loader's bots ref stays private to the hook; nothing else read it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
This commit is contained in:
parent
2b27d5c5da
commit
cf3f1ecc3f
@ -7,14 +7,11 @@ import {
|
||||
import { CSS as DndCSS } from "@dnd-kit/utilities";
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import type {
|
||||
AgentSkillCatalogEntry,
|
||||
Bot,
|
||||
BotSection,
|
||||
ComputerMode,
|
||||
ComputerReleaseReason,
|
||||
ComputerStatus,
|
||||
Connection,
|
||||
ConnectionCatalogItem,
|
||||
Group,
|
||||
MainConversationRef,
|
||||
Me,
|
||||
@ -34,7 +31,6 @@ import {
|
||||
import {
|
||||
applyWorkspaceFocus,
|
||||
attachmentsForThread,
|
||||
buildComposerMentionOptions,
|
||||
type ComposerMention,
|
||||
cronFromPreset,
|
||||
deriveWorkspaceFocus,
|
||||
@ -235,10 +231,13 @@ import {
|
||||
threadSnapshotSignal,
|
||||
} from "./shell/thread-state";
|
||||
import { Transcript } from "./shell/transcript";
|
||||
import { useAgentSkills } from "./shell/use-agent-skills";
|
||||
import { useBotsSidebarCollapse } from "./shell/use-bots-sidebar-collapse";
|
||||
import { useBrowserNotifications } from "./shell/use-browser-notifications";
|
||||
import { useComputerViewport } from "./shell/use-computer-viewport";
|
||||
import { useHeuteActionCount } from "./shell/use-heute-action-count";
|
||||
import { useMentionTargets } from "./shell/use-mention-targets";
|
||||
import { useMessagingStatus } from "./shell/use-messaging-status";
|
||||
import { useMobileSidebar } from "./shell/use-mobile-sidebar";
|
||||
import { useShellSounds } from "./shell/use-shell-sounds";
|
||||
import { useSidebarModel } from "./shell/use-sidebar-model";
|
||||
@ -421,16 +420,6 @@ export function ShellPage() {
|
||||
const [routinesBotId, setRoutinesBotId] = useState<string | null>(null);
|
||||
const [taughtSkills, setTaughtSkills] = useState<TaughtSkill[]>([]);
|
||||
const [taughtSkillsBotId, setTaughtSkillsBotId] = useState<string | null>(null);
|
||||
const [agentSkills, setAgentSkills] = useState<AgentSkillCatalogEntry[]>([]);
|
||||
const [mentionRoutines, setMentionRoutines] = useState<Array<Routine & { botName?: string }>>([]);
|
||||
const [mentionConnectors, setMentionConnectors] = useState<
|
||||
Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
authStatus: "connected" | "needs_auth";
|
||||
connectionId?: string;
|
||||
}>
|
||||
>([]);
|
||||
const [teachBusy, setTeachBusy] = useState(false);
|
||||
const [computer, setComputer] = useState<ComputerStatus | null>(null);
|
||||
const computerRef = useRef<ComputerStatus | null>(null);
|
||||
@ -488,8 +477,6 @@ export function ShellPage() {
|
||||
|
||||
const [settingsSection, setSettingsSection] = useState<SettingsSection>("general");
|
||||
const [messagingSettingsOpen, setMessagingSettingsOpen] = useState(false);
|
||||
const [messagingSurfaceEnabled, setMessagingSurfaceEnabled] = useState(false);
|
||||
const [messagingProviders, setMessagingProviders] = useState<string[]>([]);
|
||||
const [memoryProviderConfig, setMemoryProviderConfig] = useState<
|
||||
SpaceMemoryConfig | null | undefined
|
||||
>(undefined);
|
||||
@ -595,22 +582,7 @@ export function ShellPage() {
|
||||
// takeover failures must stay visible even when a URL remains.
|
||||
const [computerErrorFromScreen, setComputerErrorFromScreen] = useState(false);
|
||||
const computerViewport = useComputerViewport(computerOpen);
|
||||
useEffect(() => {
|
||||
if (!session.data?.user) return;
|
||||
let cancelled = false;
|
||||
void rpc.messaging
|
||||
.status()
|
||||
.then((status) => {
|
||||
if (!cancelled) {
|
||||
setMessagingSurfaceEnabled(status.enabled);
|
||||
setMessagingProviders(status.providers);
|
||||
}
|
||||
})
|
||||
.catch(() => undefined);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [session.data?.user]);
|
||||
const { messagingSurfaceEnabled, messagingProviders } = useMessagingStatus(session);
|
||||
const [usage, setUsage] = useState<{
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
@ -1093,27 +1065,7 @@ export function ShellPage() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void rpc.agentSkills
|
||||
.list()
|
||||
.then((skills) => {
|
||||
if (!cancelled) setAgentSkills(skills);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setAgentSkills([]);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refreshAgentSkills = useCallback(() => {
|
||||
void rpc.agentSkills
|
||||
.list()
|
||||
.then(setAgentSkills)
|
||||
.catch(() => undefined);
|
||||
}, []);
|
||||
const { agentSkills, setAgentSkills, refreshAgentSkills } = useAgentSkills();
|
||||
|
||||
useEffect(() => {
|
||||
void rpc.voice
|
||||
@ -1869,48 +1821,15 @@ export function ShellPage() {
|
||||
? t`You`
|
||||
: (resolveTranscriptMemberName(activeReplyTarget.botId) ?? active?.name ?? t`Bot`)
|
||||
: undefined;
|
||||
const composerMentionTargets = useMemo(() => {
|
||||
const mentionBots = new Map(
|
||||
bots.map((bot) => [bot.id, { id: bot.id, name: bot.name, color: bot.color }]),
|
||||
);
|
||||
// Group members stay mentionable when the sidebar bot list is still catching up
|
||||
// after creating the group and remounting the shell.
|
||||
if (inGroup) {
|
||||
for (const member of [...(activeGroup?.members ?? []), ...(activeSnapshot?.members ?? [])]) {
|
||||
if (!mentionBots.has(member.botId)) {
|
||||
mentionBots.set(member.botId, {
|
||||
id: member.botId,
|
||||
name: member.name,
|
||||
color: member.color,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return buildComposerMentionOptions({
|
||||
query: "",
|
||||
includeEveryone: inGroup,
|
||||
currentGroupId: groupId,
|
||||
bots: [...mentionBots.values()],
|
||||
groups: groups.map((group) => ({ id: group.id, name: group.name })),
|
||||
routines: mentionRoutines.map((routine) => ({
|
||||
id: routine.id,
|
||||
name: routine.name,
|
||||
crons: routine.crons,
|
||||
botId: routine.botId,
|
||||
botName: routine.botName,
|
||||
})),
|
||||
connectors: mentionConnectors,
|
||||
});
|
||||
}, [
|
||||
activeGroup?.members,
|
||||
activeSnapshot?.members,
|
||||
const composerMentionTargets = useMentionTargets({
|
||||
bots,
|
||||
groupId,
|
||||
groups,
|
||||
groupId,
|
||||
inGroup,
|
||||
mentionConnectors,
|
||||
mentionRoutines,
|
||||
]);
|
||||
activeGroup,
|
||||
activeSnapshot,
|
||||
initialBotsLoaded,
|
||||
});
|
||||
const shellReady =
|
||||
initialBotsLoaded &&
|
||||
(inGroup
|
||||
@ -1925,78 +1844,6 @@ export function ShellPage() {
|
||||
const jumpToMessageRef = useRef(jumpToMessage);
|
||||
jumpToMessageRef.current = jumpToMessage;
|
||||
|
||||
const mentionBotsKey = useMemo(
|
||||
() => bots.map((bot) => `${bot.id}:${bot.name}`).join(","),
|
||||
[bots],
|
||||
);
|
||||
const botsForMentionsRef = useRef(bots);
|
||||
botsForMentionsRef.current = bots;
|
||||
|
||||
useEffect(() => {
|
||||
const bots = botsForMentionsRef.current;
|
||||
if (!initialBotsLoaded || bots.length === 0) {
|
||||
setMentionRoutines([]);
|
||||
setMentionConnectors([]);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const botNameById = new Map(bots.map((bot) => [bot.id, bot.name]));
|
||||
void Promise.all(
|
||||
bots.map((bot) =>
|
||||
rpc.routines
|
||||
.list({ botId: bot.id })
|
||||
.then((rows) =>
|
||||
rows.map((routine) => ({
|
||||
...routine,
|
||||
botName: botNameById.get(bot.id) ?? bot.name,
|
||||
})),
|
||||
)
|
||||
.catch(() => [] as Array<Routine & { botName?: string }>),
|
||||
),
|
||||
).then((lists) => {
|
||||
if (!cancelled) setMentionRoutines(lists.flat());
|
||||
});
|
||||
void Promise.all([
|
||||
rpc.connections.list().catch(() => [] as Connection[]),
|
||||
rpc.connections.catalog({}).catch(() => [] as ConnectionCatalogItem[]),
|
||||
]).then(([connections, catalog]) => {
|
||||
if (cancelled) return;
|
||||
const connected = connections.filter((row) => row.status === "connected");
|
||||
const options: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
authStatus: "connected" | "needs_auth";
|
||||
connectionId?: string;
|
||||
}> = connected.map((row) => ({
|
||||
id: row.id,
|
||||
name: row.displayName,
|
||||
authStatus: "connected" as const,
|
||||
connectionId: row.id,
|
||||
}));
|
||||
for (const item of catalog) {
|
||||
if (item.connected || item.noAuth) continue;
|
||||
if (
|
||||
connected.some(
|
||||
(row) =>
|
||||
row.provider.toLowerCase() === item.slug.toLowerCase() ||
|
||||
row.displayName.toLowerCase() === item.name.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
options.push({
|
||||
id: `catalog:${item.connectorId}:${item.slug}`,
|
||||
name: item.name,
|
||||
authStatus: "needs_auth",
|
||||
});
|
||||
}
|
||||
setMentionConnectors(options);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [initialBotsLoaded, mentionBotsKey]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (initialBotsLoaded) {
|
||||
markOnce("rk:renderer:bots-committed");
|
||||
|
||||
31
apps/web/src/pages/shell/use-agent-skills.ts
Normal file
31
apps/web/src/pages/shell/use-agent-skills.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { AgentSkillCatalogEntry } from "@rakazo/contracts";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { rpc } from "../../lib/rpc";
|
||||
|
||||
export function useAgentSkills() {
|
||||
const [agentSkills, setAgentSkills] = useState<AgentSkillCatalogEntry[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void rpc.agentSkills
|
||||
.list()
|
||||
.then((skills) => {
|
||||
if (!cancelled) setAgentSkills(skills);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setAgentSkills([]);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refreshAgentSkills = useCallback(() => {
|
||||
void rpc.agentSkills
|
||||
.list()
|
||||
.then(setAgentSkills)
|
||||
.catch(() => undefined);
|
||||
}, []);
|
||||
|
||||
return { agentSkills, setAgentSkills, refreshAgentSkills };
|
||||
}
|
||||
159
apps/web/src/pages/shell/use-mention-targets.ts
Normal file
159
apps/web/src/pages/shell/use-mention-targets.ts
Normal file
@ -0,0 +1,159 @@
|
||||
import type {
|
||||
Bot,
|
||||
Connection,
|
||||
ConnectionCatalogItem,
|
||||
Group,
|
||||
Routine,
|
||||
ThreadSnapshot,
|
||||
} from "@rakazo/contracts";
|
||||
import type { ComposerMention } from "@rakazo/core";
|
||||
import { buildComposerMentionOptions } from "@rakazo/core";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { rpc } from "../../lib/rpc";
|
||||
|
||||
export type MentionTargetsInput = {
|
||||
bots: Bot[];
|
||||
groups: Group[];
|
||||
groupId: string | undefined;
|
||||
inGroup: boolean;
|
||||
activeGroup: Group | undefined;
|
||||
activeSnapshot: ThreadSnapshot | null;
|
||||
initialBotsLoaded: boolean;
|
||||
};
|
||||
|
||||
export function useMentionTargets({
|
||||
bots,
|
||||
groups,
|
||||
groupId,
|
||||
inGroup,
|
||||
activeGroup,
|
||||
activeSnapshot,
|
||||
initialBotsLoaded,
|
||||
}: MentionTargetsInput): ComposerMention[] {
|
||||
const [mentionRoutines, setMentionRoutines] = useState<Array<Routine & { botName?: string }>>([]);
|
||||
const [mentionConnectors, setMentionConnectors] = useState<
|
||||
Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
authStatus: "connected" | "needs_auth";
|
||||
connectionId?: string;
|
||||
}>
|
||||
>([]);
|
||||
|
||||
const composerMentionTargets = useMemo(() => {
|
||||
const mentionBots = new Map(
|
||||
bots.map((bot) => [bot.id, { id: bot.id, name: bot.name, color: bot.color }]),
|
||||
);
|
||||
// Group members stay mentionable when the sidebar bot list is still catching up
|
||||
// after creating the group and remounting the shell.
|
||||
if (inGroup) {
|
||||
for (const member of [...(activeGroup?.members ?? []), ...(activeSnapshot?.members ?? [])]) {
|
||||
if (!mentionBots.has(member.botId)) {
|
||||
mentionBots.set(member.botId, {
|
||||
id: member.botId,
|
||||
name: member.name,
|
||||
color: member.color,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return buildComposerMentionOptions({
|
||||
query: "",
|
||||
includeEveryone: inGroup,
|
||||
currentGroupId: groupId,
|
||||
bots: [...mentionBots.values()],
|
||||
groups: groups.map((group) => ({ id: group.id, name: group.name })),
|
||||
routines: mentionRoutines.map((routine) => ({
|
||||
id: routine.id,
|
||||
name: routine.name,
|
||||
crons: routine.crons,
|
||||
botId: routine.botId,
|
||||
botName: routine.botName,
|
||||
})),
|
||||
connectors: mentionConnectors,
|
||||
});
|
||||
}, [
|
||||
activeGroup?.members,
|
||||
activeSnapshot?.members,
|
||||
bots,
|
||||
groupId,
|
||||
groups,
|
||||
inGroup,
|
||||
mentionConnectors,
|
||||
mentionRoutines,
|
||||
]);
|
||||
|
||||
const mentionBotsKey = useMemo(
|
||||
() => bots.map((bot) => `${bot.id}:${bot.name}`).join(","),
|
||||
[bots],
|
||||
);
|
||||
const botsForMentionsRef = useRef(bots);
|
||||
botsForMentionsRef.current = bots;
|
||||
|
||||
useEffect(() => {
|
||||
const bots = botsForMentionsRef.current;
|
||||
if (!initialBotsLoaded || bots.length === 0) {
|
||||
setMentionRoutines([]);
|
||||
setMentionConnectors([]);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const botNameById = new Map(bots.map((bot) => [bot.id, bot.name]));
|
||||
void Promise.all(
|
||||
bots.map((bot) =>
|
||||
rpc.routines
|
||||
.list({ botId: bot.id })
|
||||
.then((rows) =>
|
||||
rows.map((routine) => ({
|
||||
...routine,
|
||||
botName: botNameById.get(bot.id) ?? bot.name,
|
||||
})),
|
||||
)
|
||||
.catch(() => [] as Array<Routine & { botName?: string }>),
|
||||
),
|
||||
).then((lists) => {
|
||||
if (!cancelled) setMentionRoutines(lists.flat());
|
||||
});
|
||||
void Promise.all([
|
||||
rpc.connections.list().catch(() => [] as Connection[]),
|
||||
rpc.connections.catalog({}).catch(() => [] as ConnectionCatalogItem[]),
|
||||
]).then(([connections, catalog]) => {
|
||||
if (cancelled) return;
|
||||
const connected = connections.filter((row) => row.status === "connected");
|
||||
const options: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
authStatus: "connected" | "needs_auth";
|
||||
connectionId?: string;
|
||||
}> = connected.map((row) => ({
|
||||
id: row.id,
|
||||
name: row.displayName,
|
||||
authStatus: "connected" as const,
|
||||
connectionId: row.id,
|
||||
}));
|
||||
for (const item of catalog) {
|
||||
if (item.connected || item.noAuth) continue;
|
||||
if (
|
||||
connected.some(
|
||||
(row) =>
|
||||
row.provider.toLowerCase() === item.slug.toLowerCase() ||
|
||||
row.displayName.toLowerCase() === item.name.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
options.push({
|
||||
id: `catalog:${item.connectorId}:${item.slug}`,
|
||||
name: item.name,
|
||||
authStatus: "needs_auth",
|
||||
});
|
||||
}
|
||||
setMentionConnectors(options);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [initialBotsLoaded, mentionBotsKey]);
|
||||
|
||||
return composerMentionTargets;
|
||||
}
|
||||
27
apps/web/src/pages/shell/use-messaging-status.ts
Normal file
27
apps/web/src/pages/shell/use-messaging-status.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { authClient } from "../../lib/auth";
|
||||
import { rpc } from "../../lib/rpc";
|
||||
|
||||
export function useMessagingStatus(session: ReturnType<typeof authClient.useSession>) {
|
||||
const [messagingSurfaceEnabled, setMessagingSurfaceEnabled] = useState(false);
|
||||
const [messagingProviders, setMessagingProviders] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session.data?.user) return;
|
||||
let cancelled = false;
|
||||
void rpc.messaging
|
||||
.status()
|
||||
.then((status) => {
|
||||
if (!cancelled) {
|
||||
setMessagingSurfaceEnabled(status.enabled);
|
||||
setMessagingProviders(status.providers);
|
||||
}
|
||||
})
|
||||
.catch(() => undefined);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [session.data?.user]);
|
||||
|
||||
return { messagingSurfaceEnabled, messagingProviders };
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user