@nextclaw/ui 0.11.23 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/dist/assets/{ChannelsList-DVDu1xvz.js → ChannelsList-DekMP4a3.js} +1 -1
- package/dist/assets/ChatPage-Dgw4vlDt.js +43 -0
- package/dist/assets/DocBrowser-CExjX5is.js +1 -0
- package/dist/assets/{DocBrowser-BmtBLFU0.js → DocBrowser-DjcghYGO.js} +1 -1
- package/dist/assets/{DocBrowserContext-YIKkPb76.js → DocBrowserContext-CLlq7rZQ.js} +1 -1
- package/dist/assets/{LogoBadge-F7ZWdxLT.js → LogoBadge-D_dOy5U3.js} +1 -1
- package/dist/assets/{MarketplacePage-Buo9HrOz.js → MarketplacePage-BlIeNn3x.js} +2 -2
- package/dist/assets/MarketplacePage-DGfzg1LG.js +1 -0
- package/dist/assets/{McpMarketplacePage-JnkYwK7p.js → McpMarketplacePage-mz2_IX1O.js} +2 -2
- package/dist/assets/{ModelConfig-BYRhgp0c.js → ModelConfig-C_49_a9v.js} +1 -1
- package/dist/assets/{ProvidersList-DmLyyHvX.js → ProvidersList-B0RCb_Vg.js} +1 -1
- package/dist/assets/{RemoteAccessPage-CDSSvH7Z.js → RemoteAccessPage-CcfQjLtx.js} +1 -1
- package/dist/assets/RuntimeConfig-DBWzwoY-.js +1 -0
- package/dist/assets/{SearchConfig-D5f1EkLE.js → SearchConfig-jSdwlH4b.js} +1 -1
- package/dist/assets/{SecretsConfig-D61IKcYt.js → SecretsConfig-DbiS3txa.js} +1 -1
- package/dist/assets/{SessionsConfig-BRIxVTEv.js → SessionsConfig-CbIOcAp8.js} +2 -2
- package/dist/assets/{book-open-CXoF5nQC.js → book-open-BLxSL7Dk.js} +1 -1
- package/dist/assets/chat-session-display-8yW6-mtm.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-CvRWvTy5.js → chunk-JZWAC4HX-Bp0t5xoO.js} +1 -1
- package/dist/assets/{config-DJswxxE8.js → config-C96FWufn.js} +1 -1
- package/dist/assets/{createLucideIcon-CjGHOWb6.js → createLucideIcon-B_U7Nq4F.js} +1 -1
- package/dist/assets/{dist-Cl2QB-2y.js → dist-BFY-GyT4.js} +1 -1
- package/dist/assets/{dist-nqTTbVdA.js → dist-D9pHzW9z.js} +1 -1
- package/dist/assets/{external-link-tIO7zING.js → external-link-BydIQTIH.js} +1 -1
- package/dist/assets/{hash-JWUyl1pT.js → hash-Djdf0x1C.js} +1 -1
- package/dist/assets/i18n-DAekxt_G.js +1 -0
- package/dist/assets/index-CHEgQIiO.css +1 -0
- package/dist/assets/index-DqSv8Azv.js +6 -0
- package/dist/assets/{label-BIpeNu4r.js → label-Bvv4Mrea.js} +1 -1
- package/dist/assets/loader-circle-CGXXikVG.js +1 -0
- package/dist/assets/{logos-DThdM9lk.js → logos-CGJJRI5_.js} +1 -1
- package/dist/assets/{page-layout-D3Xo605Z.js → page-layout-6Nm4Cnvr.js} +1 -1
- package/dist/assets/plus-CrW9BJDy.js +1 -0
- package/dist/assets/{popover-BJRUGA_H.js → popover-b9rSYI6X.js} +1 -1
- package/dist/assets/{provider-models-bz5y28rq.js → provider-models-IJDA940D.js} +1 -1
- package/dist/assets/{react-7ZHqQtEV.js → react-CDZz_StC.js} +1 -1
- package/dist/assets/{refresh-ccw-CC6-_QuL.js → refresh-ccw-BvSSnnCw.js} +1 -1
- package/dist/assets/{save-DJM5RRWW.js → save-CAf0_-b9.js} +1 -1
- package/dist/assets/search-DgoXxocn.js +1 -0
- package/dist/assets/{security-config-DbUyWcQz.js → security-config-DF66-l25.js} +1 -1
- package/dist/assets/{select-DSkTc61S.js → select-CEIMqc0H.js} +1 -1
- package/dist/assets/skeleton-BiPUQkOD.js +1 -0
- package/dist/assets/{status-dot-LNBlDu3q.js → status-dot-CmQI5Qq2.js} +1 -1
- package/dist/assets/{switch-Bo-Y46HZ.js → switch-B7SxDXyR.js} +1 -1
- package/dist/assets/{tabs-custom-DXv507_2.js → tabs-custom-Dxt6EJJW.js} +1 -1
- package/dist/assets/{trash-2-DFZmW6Gg.js → trash-2-BnQ1PDTw.js} +1 -1
- package/dist/assets/{useConfirmDialog-COwYXDKm.js → useConfirmDialog-B-vMOmhG.js} +1 -1
- package/dist/assets/{useMutation-DrZrOgVL.js → useMutation-Bi39Z9_J.js} +1 -1
- package/dist/assets/x-PBSiWt3l.js +1 -0
- package/dist/index.html +18 -18
- package/package.json +7 -7
- package/src/App.tsx +2 -0
- package/src/api/agents.ts +26 -0
- package/src/api/types.ts +23 -5
- package/src/components/agents/AgentsPage.test.tsx +70 -0
- package/src/components/agents/AgentsPage.tsx +353 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +172 -13
- package/src/components/chat/ChatConversationPanel.tsx +30 -7
- package/src/components/chat/ChatSidebar.test.tsx +48 -0
- package/src/components/chat/ChatSidebar.tsx +11 -0
- package/src/components/chat/ChatWelcome.test.tsx +30 -0
- package/src/components/chat/ChatWelcome.tsx +50 -1
- package/src/components/chat/adapters/chat-message-part.adapter.ts +5 -0
- package/src/components/chat/adapters/chat-message-tool-agent-id.test.ts +102 -0
- package/src/components/chat/adapters/chat-message-tool-agent-id.ts +47 -0
- package/src/components/chat/adapters/chat-message.adapter.test.ts +6 -0
- package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +24 -15
- package/src/components/chat/chat-child-session-panel.tsx +115 -49
- package/src/components/chat/chat-page-runtime.test.ts +30 -0
- package/src/components/chat/chat-page-shell.tsx +8 -17
- package/src/components/chat/chat-session-route.ts +0 -14
- package/src/components/chat/chat-sidebar-session-item.tsx +20 -1
- package/src/components/chat/containers/chat-input-bar.container.tsx +2 -1
- package/src/components/chat/containers/chat-message-list.container.tsx +7 -0
- package/src/components/chat/ncp/NcpChatPage.tsx +77 -158
- package/src/components/chat/ncp/README.md +3 -0
- package/src/components/chat/ncp/ncp-chat-page-data.test.ts +2 -0
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +66 -10
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +2 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +1 -0
- package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +128 -0
- package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +52 -0
- package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.test.tsx +101 -0
- package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.ts +72 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +1 -0
- package/src/components/chat/stores/chat-thread.store.ts +20 -6
- package/src/components/common/AgentAvatar.tsx +63 -0
- package/src/components/common/agent-identity/agent-identity-avatar.tsx +27 -0
- package/src/components/common/agent-identity/index.ts +3 -0
- package/src/components/common/agent-identity/use-agent-identity.ts +50 -0
- package/src/components/config/RuntimeConfig.tsx +14 -101
- package/src/components/config/runtime-config-agent.utils.ts +95 -0
- package/src/components/layout/AppLayout.tsx +3 -1
- package/src/components/layout/Sidebar.tsx +6 -1
- package/src/components/layout/app-layout.test.tsx +30 -0
- package/src/components/ui/tabs.tsx +2 -0
- package/src/hooks/README.md +3 -0
- package/src/hooks/agents/useAgents.ts +44 -0
- package/src/lib/i18n.agents.ts +66 -0
- package/src/lib/i18n.chat.ts +5 -0
- package/src/lib/i18n.ts +4 -6
- package/src/lib/ui-document-title.ts +1 -0
- package/dist/assets/ChatPage-Z9tRzm_n.js +0 -43
- package/dist/assets/DocBrowser-B9OaZjmg.js +0 -1
- package/dist/assets/MarketplacePage-D6rVQEQR.js +0 -1
- package/dist/assets/RuntimeConfig-v7a7Fe3x.js +0 -1
- package/dist/assets/chat-session-display-D0WpnuRZ.js +0 -1
- package/dist/assets/i18n-CDHMXlRZ.js +0 -1
- package/dist/assets/index-BuwbBgmT.js +0 -6
- package/dist/assets/index-bZ8cqQIS.css +0 -1
- package/dist/assets/loader-circle-Cs8XVFTw.js +0 -1
- package/dist/assets/plus-PHf8q-Ct.js +0 -1
- package/dist/assets/search-C91yH_6y.js +0 -1
- package/dist/assets/skeleton-Dzg-HOiN.js +0 -1
- package/dist/assets/x-D7Q1yqSF.js +0 -1
- /package/src/lib/{i18n → i18n-runtime}/i18n-language-owner.ts +0 -0
- /package/src/lib/{i18n → i18n-runtime}/i18n.path-picker.ts +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { fetchNcpSessionConversationSeed, useNcpSessionConversation } from "./use-ncp-session-conversation";
|
|
4
|
+
|
|
5
|
+
const mocks = vi.hoisted(() => ({
|
|
6
|
+
fetchNcpSessionMessages: vi.fn(),
|
|
7
|
+
hydratedCalls: [] as Array<{ client: unknown }>,
|
|
8
|
+
useHydratedNcpAgent: vi.fn(() => ({
|
|
9
|
+
snapshot: {
|
|
10
|
+
messages: [],
|
|
11
|
+
streamingMessage: null,
|
|
12
|
+
activeRun: null,
|
|
13
|
+
error: null,
|
|
14
|
+
},
|
|
15
|
+
visibleMessages: [],
|
|
16
|
+
activeRunId: null,
|
|
17
|
+
isRunning: false,
|
|
18
|
+
isSending: false,
|
|
19
|
+
send: vi.fn(),
|
|
20
|
+
abort: vi.fn(),
|
|
21
|
+
streamRun: vi.fn(),
|
|
22
|
+
isHydrating: false,
|
|
23
|
+
hydrateError: null,
|
|
24
|
+
})),
|
|
25
|
+
clientInstances: [] as unknown[],
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock("@/api/ncp-session", () => ({
|
|
29
|
+
fetchNcpSessionMessages: mocks.fetchNcpSessionMessages,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock("@nextclaw/ncp-react", () => ({
|
|
33
|
+
useHydratedNcpAgent: vi.fn((params: { client: unknown }) => {
|
|
34
|
+
mocks.hydratedCalls.push(params);
|
|
35
|
+
return mocks.useHydratedNcpAgent();
|
|
36
|
+
}),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
vi.mock("@nextclaw/ncp-http-agent-client", () => ({
|
|
40
|
+
NcpHttpAgentClientEndpoint: vi.fn().mockImplementation(function MockClient(this: object) {
|
|
41
|
+
mocks.clientInstances.push(this);
|
|
42
|
+
}),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
describe("useNcpSessionConversation", () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
mocks.fetchNcpSessionMessages.mockReset();
|
|
48
|
+
mocks.useHydratedNcpAgent.mockClear();
|
|
49
|
+
mocks.hydratedCalls.length = 0;
|
|
50
|
+
mocks.clientInstances.length = 0;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("hydrates seed from the shared session messages endpoint payload", async () => {
|
|
54
|
+
mocks.fetchNcpSessionMessages.mockResolvedValue({
|
|
55
|
+
sessionId: "session-1",
|
|
56
|
+
status: "running",
|
|
57
|
+
total: 1,
|
|
58
|
+
messages: [{ id: "msg-1" }],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const result = await fetchNcpSessionConversationSeed(
|
|
62
|
+
"session-1",
|
|
63
|
+
new AbortController().signal,
|
|
64
|
+
300,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(mocks.fetchNcpSessionMessages).toHaveBeenCalledWith("session-1", 300);
|
|
68
|
+
expect(result).toEqual({
|
|
69
|
+
messages: [{ id: "msg-1" }],
|
|
70
|
+
status: "running",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("treats a missing session as an empty idle draft seed", async () => {
|
|
75
|
+
mocks.fetchNcpSessionMessages.mockRejectedValue(
|
|
76
|
+
new Error("ncp session not found: draft-session"),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const result = await fetchNcpSessionConversationSeed(
|
|
80
|
+
"draft-session",
|
|
81
|
+
new AbortController().signal,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(result).toEqual({
|
|
85
|
+
messages: [],
|
|
86
|
+
status: "idle",
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("creates an isolated endpoint instance per viewer", () => {
|
|
91
|
+
renderHook(() => useNcpSessionConversation("session-a"));
|
|
92
|
+
renderHook(() => useNcpSessionConversation("session-b"));
|
|
93
|
+
|
|
94
|
+
expect(mocks.useHydratedNcpAgent).toHaveBeenCalledTimes(2);
|
|
95
|
+
expect(mocks.clientInstances).toHaveLength(2);
|
|
96
|
+
expect(mocks.hydratedCalls).toHaveLength(2);
|
|
97
|
+
expect(mocks.clientInstances[0]).not.toBe(mocks.clientInstances[1]);
|
|
98
|
+
expect(mocks.hydratedCalls[0]?.client).toBe(mocks.clientInstances[0]);
|
|
99
|
+
expect(mocks.hydratedCalls[1]?.client).toBe(mocks.clientInstances[1]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { NcpHttpAgentClientEndpoint } from "@nextclaw/ncp-http-agent-client";
|
|
3
|
+
import { useHydratedNcpAgent, type NcpConversationSeed } from "@nextclaw/ncp-react";
|
|
4
|
+
import { API_BASE } from "@/api/api-base";
|
|
5
|
+
import { fetchNcpSessionMessages } from "@/api/ncp-session";
|
|
6
|
+
import { createNcpAppClientFetch } from "@/components/chat/ncp/ncp-app-client-fetch";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MESSAGE_LIMIT = 300;
|
|
9
|
+
|
|
10
|
+
type UseNcpSessionConversationOptions = {
|
|
11
|
+
messageLimit?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function isMissingNcpSessionError(error: unknown): boolean {
|
|
15
|
+
if (!(error instanceof Error)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return error.message.includes("ncp session not found:");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createNcpSessionConversationClient(): NcpHttpAgentClientEndpoint {
|
|
22
|
+
return new NcpHttpAgentClientEndpoint({
|
|
23
|
+
baseUrl: API_BASE,
|
|
24
|
+
basePath: "/api/ncp/agent",
|
|
25
|
+
fetchImpl: createNcpAppClientFetch(),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function fetchNcpSessionConversationSeed(
|
|
30
|
+
sessionId: string,
|
|
31
|
+
signal: AbortSignal,
|
|
32
|
+
messageLimit = DEFAULT_MESSAGE_LIMIT,
|
|
33
|
+
): Promise<NcpConversationSeed> {
|
|
34
|
+
signal.throwIfAborted();
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetchNcpSessionMessages(sessionId, messageLimit);
|
|
38
|
+
signal.throwIfAborted();
|
|
39
|
+
return {
|
|
40
|
+
messages: response.messages,
|
|
41
|
+
status: response.status ?? "idle",
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
signal.throwIfAborted();
|
|
45
|
+
if (!isMissingNcpSessionError(error)) {
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
messages: [],
|
|
50
|
+
status: "idle",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useNcpSessionConversation(
|
|
56
|
+
sessionId: string,
|
|
57
|
+
options: UseNcpSessionConversationOptions = {},
|
|
58
|
+
) {
|
|
59
|
+
const [client] = useState(() => createNcpSessionConversationClient());
|
|
60
|
+
const messageLimit = options.messageLimit ?? DEFAULT_MESSAGE_LIMIT;
|
|
61
|
+
const loadSeed = useCallback(
|
|
62
|
+
(targetSessionId: string, signal: AbortSignal) =>
|
|
63
|
+
fetchNcpSessionConversationSeed(targetSessionId, signal, messageLimit),
|
|
64
|
+
[messageLimit],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return useHydratedNcpAgent({
|
|
68
|
+
sessionId,
|
|
69
|
+
client,
|
|
70
|
+
loadSeed,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -38,6 +38,7 @@ export type ChatThreadManagerLike = {
|
|
|
38
38
|
createSession: () => void;
|
|
39
39
|
goToProviders: () => void;
|
|
40
40
|
openSessionFromToolAction: (action: ChatToolActionViewModel) => void;
|
|
41
|
+
selectChildSessionDetail: (sessionKey: string) => void;
|
|
41
42
|
closeChildSessionDetail: () => void;
|
|
42
43
|
goToParentSession: () => void;
|
|
43
44
|
};
|
|
@@ -2,6 +2,14 @@ import { create } from 'zustand';
|
|
|
2
2
|
import type { MutableRefObject } from 'react';
|
|
3
3
|
import type { NcpMessage } from '@nextclaw/ncp';
|
|
4
4
|
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
5
|
+
import type { AgentProfileView } from '@/api/types';
|
|
6
|
+
|
|
7
|
+
export type ChatChildSessionTab = {
|
|
8
|
+
sessionKey: string;
|
|
9
|
+
parentSessionKey: string | null;
|
|
10
|
+
label?: string | null;
|
|
11
|
+
agentId?: string | null;
|
|
12
|
+
};
|
|
5
13
|
|
|
6
14
|
export type ChatThreadSnapshot = {
|
|
7
15
|
isProviderStateResolved: boolean;
|
|
@@ -10,6 +18,10 @@ export type ChatThreadSnapshot = {
|
|
|
10
18
|
sessionTypeUnavailableMessage?: string | null;
|
|
11
19
|
sessionTypeLabel?: string | null;
|
|
12
20
|
sessionKey: string | null;
|
|
21
|
+
agentId?: string | null;
|
|
22
|
+
agentDisplayName?: string | null;
|
|
23
|
+
agentAvatarUrl?: string | null;
|
|
24
|
+
availableAgents?: AgentProfileView[];
|
|
13
25
|
sessionDisplayName?: string;
|
|
14
26
|
sessionProjectRoot?: string | null;
|
|
15
27
|
sessionProjectName?: string | null;
|
|
@@ -22,9 +34,8 @@ export type ChatThreadSnapshot = {
|
|
|
22
34
|
isAwaitingAssistantOutput: boolean;
|
|
23
35
|
parentSessionKey?: string | null;
|
|
24
36
|
parentSessionLabel?: string | null;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
childSessionDetailLabel?: string | null;
|
|
37
|
+
childSessionTabs: ChatChildSessionTab[];
|
|
38
|
+
activeChildSessionKey?: string | null;
|
|
28
39
|
};
|
|
29
40
|
|
|
30
41
|
type ChatThreadStore = {
|
|
@@ -39,6 +50,10 @@ const initialSnapshot: ChatThreadSnapshot = {
|
|
|
39
50
|
sessionTypeUnavailableMessage: null,
|
|
40
51
|
sessionTypeLabel: null,
|
|
41
52
|
sessionKey: null,
|
|
53
|
+
agentId: null,
|
|
54
|
+
agentDisplayName: null,
|
|
55
|
+
agentAvatarUrl: null,
|
|
56
|
+
availableAgents: [],
|
|
42
57
|
sessionDisplayName: undefined,
|
|
43
58
|
sessionProjectRoot: null,
|
|
44
59
|
sessionProjectName: null,
|
|
@@ -51,9 +66,8 @@ const initialSnapshot: ChatThreadSnapshot = {
|
|
|
51
66
|
isAwaitingAssistantOutput: false,
|
|
52
67
|
parentSessionKey: null,
|
|
53
68
|
parentSessionLabel: null,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
childSessionDetailLabel: null,
|
|
69
|
+
childSessionTabs: [],
|
|
70
|
+
activeChildSessionKey: null,
|
|
57
71
|
};
|
|
58
72
|
|
|
59
73
|
export const useChatThreadStore = create<ChatThreadStore>((set) => ({
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
|
|
3
|
+
type AgentAvatarProps = {
|
|
4
|
+
agentId: string;
|
|
5
|
+
displayName?: string | null;
|
|
6
|
+
avatarUrl?: string | null;
|
|
7
|
+
className?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const PALETTE = [
|
|
11
|
+
['bg-amber-100', 'text-amber-700'],
|
|
12
|
+
['bg-emerald-100', 'text-emerald-700'],
|
|
13
|
+
['bg-blue-100', 'text-blue-700'],
|
|
14
|
+
['bg-rose-100', 'text-rose-700'],
|
|
15
|
+
['bg-cyan-100', 'text-cyan-700'],
|
|
16
|
+
['bg-violet-100', 'text-violet-700']
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
function hashText(value: string): number {
|
|
20
|
+
let hash = 0;
|
|
21
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
22
|
+
hash = (hash << 5) - hash + value.charCodeAt(index);
|
|
23
|
+
hash |= 0;
|
|
24
|
+
}
|
|
25
|
+
return Math.abs(hash);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveLetter(value: string): string {
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
if (!trimmed) {
|
|
31
|
+
return 'A';
|
|
32
|
+
}
|
|
33
|
+
return trimmed.slice(0, 1).toUpperCase();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function AgentAvatar({ agentId, displayName, avatarUrl, className }: AgentAvatarProps) {
|
|
37
|
+
const seed = displayName?.trim() || agentId;
|
|
38
|
+
const [bgClass, textClass] = PALETTE[hashText(agentId) % PALETTE.length] ?? PALETTE[0];
|
|
39
|
+
|
|
40
|
+
if (avatarUrl?.trim()) {
|
|
41
|
+
return (
|
|
42
|
+
<img
|
|
43
|
+
src={avatarUrl}
|
|
44
|
+
alt={displayName?.trim() || agentId}
|
|
45
|
+
className={cn('rounded-full object-cover', className)}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className={cn(
|
|
53
|
+
'rounded-full flex items-center justify-center font-semibold',
|
|
54
|
+
bgClass,
|
|
55
|
+
textClass,
|
|
56
|
+
className
|
|
57
|
+
)}
|
|
58
|
+
aria-label={displayName?.trim() || agentId}
|
|
59
|
+
>
|
|
60
|
+
{resolveLetter(seed)}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AgentAvatar } from '@/components/common/AgentAvatar';
|
|
2
|
+
import { useAgentIdentity } from '@/components/common/agent-identity/use-agent-identity';
|
|
3
|
+
|
|
4
|
+
type AgentIdentityAvatarProps = {
|
|
5
|
+
agentId?: string | null;
|
|
6
|
+
className?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function AgentIdentityAvatar({
|
|
10
|
+
agentId,
|
|
11
|
+
className,
|
|
12
|
+
}: AgentIdentityAvatarProps) {
|
|
13
|
+
const identity = useAgentIdentity(agentId);
|
|
14
|
+
|
|
15
|
+
if (!identity) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<AgentAvatar
|
|
21
|
+
agentId={identity.agentId}
|
|
22
|
+
displayName={identity.displayName}
|
|
23
|
+
avatarUrl={identity.avatarUrl}
|
|
24
|
+
className={className}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { AgentIdentityAvatar } from '@/components/common/agent-identity/agent-identity-avatar';
|
|
2
|
+
export { useAgentIdentity } from '@/components/common/agent-identity/use-agent-identity';
|
|
3
|
+
export type { ResolvedAgentIdentity } from '@/components/common/agent-identity/use-agent-identity';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { AgentProfileView } from '@/api/types';
|
|
3
|
+
import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
|
|
4
|
+
import { useAgents } from '@/hooks/agents/useAgents';
|
|
5
|
+
|
|
6
|
+
export type ResolvedAgentIdentity = {
|
|
7
|
+
agentId: string;
|
|
8
|
+
profile: AgentProfileView | null;
|
|
9
|
+
displayName: string;
|
|
10
|
+
avatarUrl: string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function buildAgentProfileMap(
|
|
14
|
+
scopedAgents: readonly AgentProfileView[],
|
|
15
|
+
queryAgents: readonly AgentProfileView[],
|
|
16
|
+
): Map<string, AgentProfileView> {
|
|
17
|
+
return new Map(
|
|
18
|
+
[...queryAgents, ...scopedAgents]
|
|
19
|
+
.filter((agent) => typeof agent.id === 'string' && agent.id.trim().length > 0)
|
|
20
|
+
.map((agent) => [agent.id, agent]),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useAgentIdentity(
|
|
25
|
+
agentId?: string | null,
|
|
26
|
+
): ResolvedAgentIdentity | null {
|
|
27
|
+
const normalizedAgentId = agentId?.trim() ?? '';
|
|
28
|
+
const scopedAgents = useChatThreadStore(
|
|
29
|
+
(state) => state.snapshot.availableAgents ?? [],
|
|
30
|
+
);
|
|
31
|
+
const agentsQuery = useAgents();
|
|
32
|
+
|
|
33
|
+
const agentById = useMemo(
|
|
34
|
+
() => buildAgentProfileMap(scopedAgents, agentsQuery.data?.agents ?? []),
|
|
35
|
+
[agentsQuery.data?.agents, scopedAgents],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return useMemo(() => {
|
|
39
|
+
if (!normalizedAgentId) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const profile = agentById.get(normalizedAgentId) ?? null;
|
|
43
|
+
return {
|
|
44
|
+
agentId: normalizedAgentId,
|
|
45
|
+
profile,
|
|
46
|
+
displayName: profile?.displayName?.trim() || normalizedAgentId,
|
|
47
|
+
avatarUrl: profile?.avatarUrl?.trim() || null,
|
|
48
|
+
};
|
|
49
|
+
}, [agentById, normalizedAgentId]);
|
|
50
|
+
}
|
|
@@ -6,6 +6,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|
|
6
6
|
import { Input } from '@/components/ui/input';
|
|
7
7
|
import { Switch } from '@/components/ui/switch';
|
|
8
8
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
9
|
+
import {
|
|
10
|
+
createEmptyRuntimeAgent,
|
|
11
|
+
createEmptyRuntimeBinding,
|
|
12
|
+
hydrateRuntimeAgent,
|
|
13
|
+
hydrateRuntimeBinding,
|
|
14
|
+
parseOptionalInt,
|
|
15
|
+
toPersistedRuntimeAgent
|
|
16
|
+
} from '@/components/config/runtime-config-agent.utils';
|
|
9
17
|
import { hintForPath } from '@/lib/config-hints';
|
|
10
18
|
import { t } from '@/lib/i18n';
|
|
11
19
|
import { PageLayout, PageHeader } from '@/components/layout/page-layout';
|
|
@@ -22,37 +30,6 @@ const DM_SCOPE_OPTIONS: Array<{ value: DmScope; label: string }> = [
|
|
|
22
30
|
{ value: 'per-account-channel-peer', label: 'per-account-channel-peer' }
|
|
23
31
|
];
|
|
24
32
|
|
|
25
|
-
function createEmptyAgent(): AgentProfileView {
|
|
26
|
-
return {
|
|
27
|
-
id: '',
|
|
28
|
-
default: false,
|
|
29
|
-
workspace: '',
|
|
30
|
-
model: '',
|
|
31
|
-
engine: '',
|
|
32
|
-
contextTokens: undefined,
|
|
33
|
-
maxToolIterations: undefined
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function createEmptyBinding(): AgentBindingView {
|
|
38
|
-
return {
|
|
39
|
-
agentId: '',
|
|
40
|
-
match: {
|
|
41
|
-
channel: '',
|
|
42
|
-
accountId: ''
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function parseOptionalInt(value: string): number | undefined {
|
|
48
|
-
const trimmed = value.trim();
|
|
49
|
-
if (!trimmed) {
|
|
50
|
-
return undefined;
|
|
51
|
-
}
|
|
52
|
-
const parsed = Number.parseInt(trimmed, 10);
|
|
53
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
33
|
export function RuntimeConfig() {
|
|
57
34
|
const { data: config, isLoading } = useConfig();
|
|
58
35
|
const { data: schema } = useConfigSchema();
|
|
@@ -61,7 +38,6 @@ export function RuntimeConfig() {
|
|
|
61
38
|
const [agents, setAgents] = useState<AgentProfileView[]>([]);
|
|
62
39
|
const [bindings, setBindings] = useState<AgentBindingView[]>([]);
|
|
63
40
|
const [dmScope, setDmScope] = useState<DmScope>('per-channel-peer');
|
|
64
|
-
const [maxPingPongTurns, setMaxPingPongTurns] = useState(0);
|
|
65
41
|
const [defaultContextTokens, setDefaultContextTokens] = useState(200000);
|
|
66
42
|
const [defaultEngine, setDefaultEngine] = useState('native');
|
|
67
43
|
|
|
@@ -69,41 +45,15 @@ export function RuntimeConfig() {
|
|
|
69
45
|
if (!config) {
|
|
70
46
|
return;
|
|
71
47
|
}
|
|
72
|
-
setAgents(
|
|
73
|
-
|
|
74
|
-
id: agent.id ?? '',
|
|
75
|
-
default: Boolean(agent.default),
|
|
76
|
-
workspace: agent.workspace ?? '',
|
|
77
|
-
model: agent.model ?? '',
|
|
78
|
-
engine: agent.engine ?? '',
|
|
79
|
-
contextTokens: agent.contextTokens,
|
|
80
|
-
maxToolIterations: agent.maxToolIterations
|
|
81
|
-
}))
|
|
82
|
-
);
|
|
83
|
-
setBindings(
|
|
84
|
-
(config.bindings ?? []).map((binding) => ({
|
|
85
|
-
agentId: binding.agentId ?? '',
|
|
86
|
-
match: {
|
|
87
|
-
channel: binding.match?.channel ?? '',
|
|
88
|
-
accountId: binding.match?.accountId ?? '',
|
|
89
|
-
peer: binding.match?.peer
|
|
90
|
-
? {
|
|
91
|
-
kind: binding.match.peer.kind,
|
|
92
|
-
id: binding.match.peer.id
|
|
93
|
-
}
|
|
94
|
-
: undefined
|
|
95
|
-
}
|
|
96
|
-
}))
|
|
97
|
-
);
|
|
48
|
+
setAgents((config.agents.list ?? []).map(hydrateRuntimeAgent));
|
|
49
|
+
setBindings((config.bindings ?? []).map(hydrateRuntimeBinding));
|
|
98
50
|
setDmScope((config.session?.dmScope as DmScope) ?? 'per-channel-peer');
|
|
99
|
-
setMaxPingPongTurns(config.session?.agentToAgent?.maxPingPongTurns ?? 0);
|
|
100
51
|
setDefaultContextTokens(config.agents.defaults.contextTokens ?? 200000);
|
|
101
52
|
setDefaultEngine(config.agents.defaults.engine ?? 'native');
|
|
102
53
|
}, [config]);
|
|
103
54
|
|
|
104
55
|
const uiHints = schema?.uiHints;
|
|
105
56
|
const dmScopeHint = hintForPath('session.dmScope', uiHints);
|
|
106
|
-
const maxPingHint = hintForPath('session.agentToAgent.maxPingPongTurns', uiHints);
|
|
107
57
|
const defaultContextTokensHint = hintForPath('agents.defaults.contextTokens', uiHints);
|
|
108
58
|
const defaultEngineHint = hintForPath('agents.defaults.engine', uiHints);
|
|
109
59
|
const agentContextTokensHint = hintForPath('agents.list.*.contextTokens', uiHints);
|
|
@@ -137,26 +87,7 @@ export function RuntimeConfig() {
|
|
|
137
87
|
if (!id) {
|
|
138
88
|
throw new Error(t('agentIdRequiredError').replace('{index}', String(index)));
|
|
139
89
|
}
|
|
140
|
-
|
|
141
|
-
if (agent.default) {
|
|
142
|
-
normalized.default = true;
|
|
143
|
-
}
|
|
144
|
-
if (agent.workspace?.trim()) {
|
|
145
|
-
normalized.workspace = agent.workspace.trim();
|
|
146
|
-
}
|
|
147
|
-
if (agent.model?.trim()) {
|
|
148
|
-
normalized.model = agent.model.trim();
|
|
149
|
-
}
|
|
150
|
-
if (agent.engine?.trim()) {
|
|
151
|
-
normalized.engine = agent.engine.trim();
|
|
152
|
-
}
|
|
153
|
-
if (typeof agent.contextTokens === 'number') {
|
|
154
|
-
normalized.contextTokens = Math.max(1000, agent.contextTokens);
|
|
155
|
-
}
|
|
156
|
-
if (typeof agent.maxToolIterations === 'number') {
|
|
157
|
-
normalized.maxToolIterations = agent.maxToolIterations;
|
|
158
|
-
}
|
|
159
|
-
return normalized;
|
|
90
|
+
return toPersistedRuntimeAgent(agent);
|
|
160
91
|
});
|
|
161
92
|
|
|
162
93
|
const duplicates = normalizedAgents
|
|
@@ -219,10 +150,7 @@ export function RuntimeConfig() {
|
|
|
219
150
|
},
|
|
220
151
|
bindings: normalizedBindings,
|
|
221
152
|
session: {
|
|
222
|
-
dmScope
|
|
223
|
-
agentToAgent: {
|
|
224
|
-
maxPingPongTurns: Math.min(5, Math.max(0, maxPingPongTurns))
|
|
225
|
-
}
|
|
153
|
+
dmScope
|
|
226
154
|
}
|
|
227
155
|
}
|
|
228
156
|
});
|
|
@@ -289,21 +217,6 @@ export function RuntimeConfig() {
|
|
|
289
217
|
</SelectContent>
|
|
290
218
|
</Select>
|
|
291
219
|
</div>
|
|
292
|
-
<div className="space-y-2">
|
|
293
|
-
<label className="text-sm font-medium text-gray-800">
|
|
294
|
-
{maxPingHint?.label ?? t('maxPingPongTurns')}
|
|
295
|
-
</label>
|
|
296
|
-
<Input
|
|
297
|
-
type="number"
|
|
298
|
-
min={0}
|
|
299
|
-
max={5}
|
|
300
|
-
value={maxPingPongTurns}
|
|
301
|
-
onChange={(event) => setMaxPingPongTurns(Math.max(0, Number.parseInt(event.target.value, 10) || 0))}
|
|
302
|
-
/>
|
|
303
|
-
<p className="text-xs text-gray-500">
|
|
304
|
-
{maxPingHint?.help ?? t('maxPingPongTurnsHelp')}
|
|
305
|
-
</p>
|
|
306
|
-
</div>
|
|
307
220
|
</CardContent>
|
|
308
221
|
</Card>
|
|
309
222
|
|
|
@@ -389,7 +302,7 @@ export function RuntimeConfig() {
|
|
|
389
302
|
</div>
|
|
390
303
|
))}
|
|
391
304
|
|
|
392
|
-
<Button type="button" variant="outline" onClick={() => setAgents((prev) => [...prev,
|
|
305
|
+
<Button type="button" variant="outline" onClick={() => setAgents((prev) => [...prev, createEmptyRuntimeAgent()])}>
|
|
393
306
|
<Plus className="h-4 w-4 mr-2" />
|
|
394
307
|
{t('addAgent')}
|
|
395
308
|
</Button>
|
|
@@ -510,7 +423,7 @@ export function RuntimeConfig() {
|
|
|
510
423
|
);
|
|
511
424
|
})}
|
|
512
425
|
|
|
513
|
-
<Button type="button" variant="outline" onClick={() => setBindings((prev) => [...prev,
|
|
426
|
+
<Button type="button" variant="outline" onClick={() => setBindings((prev) => [...prev, createEmptyRuntimeBinding()])}>
|
|
514
427
|
<Plus className="h-4 w-4 mr-2" />
|
|
515
428
|
{t('addBinding')}
|
|
516
429
|
</Button>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { AgentBindingView, AgentProfileView } from '@/api/types';
|
|
2
|
+
|
|
3
|
+
export function createEmptyRuntimeAgent(): AgentProfileView {
|
|
4
|
+
return {
|
|
5
|
+
id: '',
|
|
6
|
+
default: false,
|
|
7
|
+
workspace: '',
|
|
8
|
+
model: '',
|
|
9
|
+
engine: '',
|
|
10
|
+
contextTokens: undefined,
|
|
11
|
+
maxToolIterations: undefined
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createEmptyRuntimeBinding(): AgentBindingView {
|
|
16
|
+
return {
|
|
17
|
+
agentId: '',
|
|
18
|
+
match: {
|
|
19
|
+
channel: '',
|
|
20
|
+
accountId: ''
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function hydrateRuntimeAgent(agent: AgentProfileView): AgentProfileView {
|
|
26
|
+
return {
|
|
27
|
+
id: agent.id ?? '',
|
|
28
|
+
default: Boolean(agent.default),
|
|
29
|
+
displayName: agent.displayName ?? '',
|
|
30
|
+
description: agent.description ?? '',
|
|
31
|
+
avatar: agent.avatar ?? '',
|
|
32
|
+
workspace: agent.workspace ?? '',
|
|
33
|
+
model: agent.model ?? '',
|
|
34
|
+
engine: agent.engine ?? '',
|
|
35
|
+
contextTokens: agent.contextTokens,
|
|
36
|
+
maxToolIterations: agent.maxToolIterations
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function hydrateRuntimeBinding(binding: AgentBindingView): AgentBindingView {
|
|
41
|
+
return {
|
|
42
|
+
agentId: binding.agentId ?? '',
|
|
43
|
+
match: {
|
|
44
|
+
channel: binding.match?.channel ?? '',
|
|
45
|
+
accountId: binding.match?.accountId ?? '',
|
|
46
|
+
peer: binding.match?.peer
|
|
47
|
+
? {
|
|
48
|
+
kind: binding.match.peer.kind,
|
|
49
|
+
id: binding.match.peer.id
|
|
50
|
+
}
|
|
51
|
+
: undefined
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function parseOptionalInt(value: string): number | undefined {
|
|
57
|
+
const trimmed = value.trim();
|
|
58
|
+
if (!trimmed) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
62
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function toPersistedRuntimeAgent(agent: AgentProfileView): AgentProfileView {
|
|
66
|
+
const normalized: AgentProfileView = { id: agent.id.trim() };
|
|
67
|
+
if (agent.default) {
|
|
68
|
+
normalized.default = true;
|
|
69
|
+
}
|
|
70
|
+
if (agent.displayName?.trim()) {
|
|
71
|
+
normalized.displayName = agent.displayName.trim();
|
|
72
|
+
}
|
|
73
|
+
if (agent.description?.trim()) {
|
|
74
|
+
normalized.description = agent.description.trim();
|
|
75
|
+
}
|
|
76
|
+
if (agent.avatar?.trim()) {
|
|
77
|
+
normalized.avatar = agent.avatar.trim();
|
|
78
|
+
}
|
|
79
|
+
if (agent.workspace?.trim()) {
|
|
80
|
+
normalized.workspace = agent.workspace.trim();
|
|
81
|
+
}
|
|
82
|
+
if (agent.model?.trim()) {
|
|
83
|
+
normalized.model = agent.model.trim();
|
|
84
|
+
}
|
|
85
|
+
if (agent.engine?.trim()) {
|
|
86
|
+
normalized.engine = agent.engine.trim();
|
|
87
|
+
}
|
|
88
|
+
if (typeof agent.contextTokens === 'number') {
|
|
89
|
+
normalized.contextTokens = Math.max(1000, agent.contextTokens);
|
|
90
|
+
}
|
|
91
|
+
if (typeof agent.maxToolIterations === 'number') {
|
|
92
|
+
normalized.maxToolIterations = agent.maxToolIterations;
|
|
93
|
+
}
|
|
94
|
+
return normalized;
|
|
95
|
+
}
|
|
@@ -21,7 +21,9 @@ function isMainWorkspaceRoute(pathname: string): boolean {
|
|
|
21
21
|
normalized === '/skills' ||
|
|
22
22
|
normalized.startsWith('/skills/') ||
|
|
23
23
|
normalized === '/cron' ||
|
|
24
|
-
normalized.startsWith('/cron/')
|
|
24
|
+
normalized.startsWith('/cron/') ||
|
|
25
|
+
normalized === '/agents' ||
|
|
26
|
+
normalized.startsWith('/agents/')
|
|
25
27
|
);
|
|
26
28
|
}
|
|
27
29
|
|