@nextclaw/ui 0.6.10 → 0.6.11
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/.eslintrc.cjs +10 -0
- package/CHANGELOG.md +9 -0
- package/dist/assets/{ChannelsList-TyMb5Mgz.js → ChannelsList-C49JQ-Zt.js} +1 -1
- package/dist/assets/ChatPage-DIx05c6s.js +36 -0
- package/dist/assets/{DocBrowser-CNtrA0ps.js → DocBrowser-CpOosDEI.js} +1 -1
- package/dist/assets/{LogoBadge-BLqiOM5D.js → LogoBadge-CL_8ZPXU.js} +1 -1
- package/dist/assets/MarketplacePage-BOzko5s9.js +49 -0
- package/dist/assets/{ModelConfig-CCsQ8KFq.js → ModelConfig-BZ4ZfaQB.js} +1 -1
- package/dist/assets/ProvidersList-fPpJ5gl6.js +1 -0
- package/dist/assets/{RuntimeConfig-BO6s-ls-.js → RuntimeConfig-Dt9pLB9P.js} +1 -1
- package/dist/assets/{SecretsConfig-mayFdxpM.js → SecretsConfig-C1PU0Yy8.js} +2 -2
- package/dist/assets/{SessionsConfig-DAIczdBj.js → SessionsConfig-EskBOofQ.js} +2 -2
- package/dist/assets/{card-BP5YnL-G.js → card-C7Gtw2Vs.js} +1 -1
- package/dist/assets/index-Cn6_2To7.js +8 -0
- package/dist/assets/{index-BUiahmWm.css → index-nEYGCJTC.css} +1 -1
- package/dist/assets/{input-B1D2QX0O.js → input-oBvxsnV9.js} +1 -1
- package/dist/assets/{label-DW0j-fXA.js → label-C7F8lMpQ.js} +1 -1
- package/dist/assets/{page-layout-Ch-H9gD-.js → page-layout-DO8BlScF.js} +1 -1
- package/dist/assets/session-run-status-Kg0FwAPn.js +3 -0
- package/dist/assets/{switch-_cZHlGKB.js → switch-C6a5GyZB.js} +1 -1
- package/dist/assets/{tabs-custom-ARxqYYjG.js → tabs-custom-BatFap5k.js} +1 -1
- package/dist/assets/{useConfirmDialog-BaU7nIat.js → useConfirmDialog-zJzVKMdu.js} +2 -2
- package/dist/assets/{vendor-C--HHaLf.js → vendor-TlME1INH.js} +84 -84
- package/dist/index.html +3 -3
- package/package.json +4 -2
- package/src/App.tsx +1 -2
- package/src/api/config.ts +199 -200
- package/src/api/types.ts +36 -24
- package/src/components/chat/ChatConversationPanel.tsx +102 -121
- package/src/components/chat/ChatPage.tsx +165 -437
- package/src/components/chat/ChatSidebar.tsx +30 -36
- package/src/components/chat/ChatThread.tsx +73 -131
- package/src/components/chat/chat-input/ChatInputBarView.tsx +82 -0
- package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +71 -0
- package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +39 -0
- package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +31 -0
- package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +112 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +24 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +58 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +56 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +40 -0
- package/src/components/chat/chat-input/useChatInputBarController.ts +313 -0
- package/src/components/chat/chat-input.types.ts +15 -0
- package/src/components/chat/chat-page-data.ts +121 -0
- package/src/components/chat/chat-page-runtime.ts +221 -0
- package/src/components/chat/chat-session-route.ts +59 -0
- package/src/components/chat/chat-stream/nextbot-parsers.ts +52 -0
- package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +413 -0
- package/src/components/chat/chat-stream/stream-event-adapter.ts +98 -0
- package/src/components/chat/chat-stream/transport.ts +159 -0
- package/src/components/chat/chat-stream/types.ts +76 -0
- package/src/components/chat/managers/chat-input.manager.ts +142 -0
- package/src/components/chat/managers/chat-run-status.manager.ts +32 -0
- package/src/components/chat/managers/chat-session-list.manager.ts +77 -0
- package/src/components/chat/managers/chat-stream-actions.manager.ts +34 -0
- package/src/components/chat/managers/chat-thread.manager.ts +86 -0
- package/src/components/chat/managers/chat-ui.manager.ts +65 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +25 -0
- package/src/components/chat/presenter/chat.presenter.ts +32 -0
- package/src/components/chat/stores/chat-input.store.ts +62 -0
- package/src/components/chat/stores/chat-run-status.store.ts +30 -0
- package/src/components/chat/stores/chat-session-list.store.ts +34 -0
- package/src/components/chat/stores/chat-thread.store.ts +52 -0
- package/src/components/chat/useChatRuntimeController.ts +134 -0
- package/src/components/chat/useChatSessionTypeState.ts +148 -0
- package/src/components/common/MaskedInput.tsx +1 -1
- package/src/hooks/useConfig.ts +31 -1
- package/src/hooks/useObservable.ts +20 -0
- package/src/lib/chat-message.ts +2 -202
- package/src/lib/chat-runtime-utils.ts +250 -0
- package/src/lib/i18n.ts +9 -0
- package/tsconfig.json +2 -1
- package/vite.config.ts +2 -1
- package/dist/assets/ChatPage-CQerYqvy.js +0 -34
- package/dist/assets/MarketplacePage-CotZxxNe.js +0 -49
- package/dist/assets/ProvidersList-BYYX5K_g.js +0 -1
- package/dist/assets/index-D6_5HaDl.js +0 -7
- package/dist/assets/session-run-status-BUYsQeWs.js +0 -5
- package/src/components/chat/ChatInputBar.tsx +0 -590
- package/src/components/chat/useChatStreamController.ts +0 -591
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import type { SessionEntryView } from '@/api/types';
|
|
3
|
+
|
|
4
|
+
export type ChatSessionListSnapshot = {
|
|
5
|
+
sessions: SessionEntryView[];
|
|
6
|
+
selectedSessionKey: string | null;
|
|
7
|
+
selectedAgentId: string;
|
|
8
|
+
query: string;
|
|
9
|
+
isLoading: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type ChatSessionListStore = {
|
|
13
|
+
snapshot: ChatSessionListSnapshot;
|
|
14
|
+
setSnapshot: (patch: Partial<ChatSessionListSnapshot>) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const initialSnapshot: ChatSessionListSnapshot = {
|
|
18
|
+
sessions: [],
|
|
19
|
+
selectedSessionKey: null,
|
|
20
|
+
selectedAgentId: 'main',
|
|
21
|
+
query: '',
|
|
22
|
+
isLoading: false
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const useChatSessionListStore = create<ChatSessionListStore>((set) => ({
|
|
26
|
+
snapshot: initialSnapshot,
|
|
27
|
+
setSnapshot: (patch) =>
|
|
28
|
+
set((state) => ({
|
|
29
|
+
snapshot: {
|
|
30
|
+
...state.snapshot,
|
|
31
|
+
...patch
|
|
32
|
+
}
|
|
33
|
+
}))
|
|
34
|
+
}));
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import type { MutableRefObject } from 'react';
|
|
3
|
+
import type { UiMessage } from '@nextclaw/agent-chat';
|
|
4
|
+
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
5
|
+
|
|
6
|
+
export type ChatThreadSnapshot = {
|
|
7
|
+
isProviderStateResolved: boolean;
|
|
8
|
+
modelOptions: ChatModelOption[];
|
|
9
|
+
sessionTypeUnavailable: boolean;
|
|
10
|
+
sessionTypeUnavailableMessage?: string | null;
|
|
11
|
+
selectedSessionKey: string | null;
|
|
12
|
+
sessionDisplayName?: string;
|
|
13
|
+
canDeleteSession: boolean;
|
|
14
|
+
isDeletePending: boolean;
|
|
15
|
+
threadRef: MutableRefObject<HTMLDivElement | null> | null;
|
|
16
|
+
isHistoryLoading: boolean;
|
|
17
|
+
uiMessages: UiMessage[];
|
|
18
|
+
isSending: boolean;
|
|
19
|
+
isAwaitingAssistantOutput: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ChatThreadStore = {
|
|
23
|
+
snapshot: ChatThreadSnapshot;
|
|
24
|
+
setSnapshot: (patch: Partial<ChatThreadSnapshot>) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const initialSnapshot: ChatThreadSnapshot = {
|
|
28
|
+
isProviderStateResolved: false,
|
|
29
|
+
modelOptions: [],
|
|
30
|
+
sessionTypeUnavailable: false,
|
|
31
|
+
sessionTypeUnavailableMessage: null,
|
|
32
|
+
selectedSessionKey: null,
|
|
33
|
+
sessionDisplayName: undefined,
|
|
34
|
+
canDeleteSession: false,
|
|
35
|
+
isDeletePending: false,
|
|
36
|
+
threadRef: null,
|
|
37
|
+
isHistoryLoading: false,
|
|
38
|
+
uiMessages: [],
|
|
39
|
+
isSending: false,
|
|
40
|
+
isAwaitingAssistantOutput: false
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const useChatThreadStore = create<ChatThreadStore>((set) => ({
|
|
44
|
+
snapshot: initialSnapshot,
|
|
45
|
+
setSnapshot: (patch) =>
|
|
46
|
+
set((state) => ({
|
|
47
|
+
snapshot: {
|
|
48
|
+
...state.snapshot,
|
|
49
|
+
...patch
|
|
50
|
+
}
|
|
51
|
+
}))
|
|
52
|
+
}));
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { type AgentChatController, getStopDisabledReason } from '@nextclaw/agent-chat';
|
|
3
|
+
import type { ChatRunView, SessionMessageView } from '@/api/types';
|
|
4
|
+
import type { SendMessageParams, UseChatStreamControllerParams } from '@/components/chat/chat-stream/types';
|
|
5
|
+
import { buildResumeMetadata, buildSendMetadata } from '@/components/chat/chat-stream/nextbot-parsers';
|
|
6
|
+
import { buildUiMessagesFromHistoryMessages, normalizeRequestedSkills } from '@/lib/chat-runtime-utils';
|
|
7
|
+
import { useValueFromBehaviorSubject, useValueFromObservable } from '@/hooks/useObservable';
|
|
8
|
+
|
|
9
|
+
export function useChatRuntimeController(
|
|
10
|
+
params: UseChatStreamControllerParams,
|
|
11
|
+
controller: AgentChatController
|
|
12
|
+
) {
|
|
13
|
+
const paramsRef = useRef(params);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
paramsRef.current = params;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const activeHistorySessionKeyRef = useRef<string | null>(null);
|
|
19
|
+
|
|
20
|
+
// Bind callbacks to controller
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
controller.setCallbacks({
|
|
23
|
+
onRunSettled: async ({ sourceSessionId, resultSessionId }) => {
|
|
24
|
+
const bindings = paramsRef.current;
|
|
25
|
+
await bindings.refetchSessions();
|
|
26
|
+
const activeSessionKey = bindings.selectedSessionKeyRef.current;
|
|
27
|
+
if (!activeSessionKey || activeSessionKey === sourceSessionId || (resultSessionId && activeSessionKey === resultSessionId)) {
|
|
28
|
+
await bindings.refetchHistory();
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
onRunError: ({ sourceMessage, restoreDraft }) => {
|
|
32
|
+
if (restoreDraft) {
|
|
33
|
+
paramsRef.current.setDraft((prev) => (prev.trim().length === 0 && sourceMessage ? sourceMessage : prev));
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
onSessionChanged: (sessionId) => {
|
|
37
|
+
paramsRef.current.setSelectedSessionKey((prev) => (prev === sessionId ? prev : sessionId));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}, [controller]);
|
|
41
|
+
|
|
42
|
+
// Reactive state from controller observables
|
|
43
|
+
const uiMessages = useValueFromObservable(controller.messages$, controller.getMessages());
|
|
44
|
+
const isSending = useValueFromBehaviorSubject(controller.isAgentResponding$);
|
|
45
|
+
const isAwaitingAssistantOutput = useValueFromBehaviorSubject(controller.isAwaitingResponse$);
|
|
46
|
+
const activeRun = useValueFromBehaviorSubject(controller.activeRun$);
|
|
47
|
+
const lastSendError = useValueFromBehaviorSubject(controller.lastError$);
|
|
48
|
+
|
|
49
|
+
// Derived state
|
|
50
|
+
const activeBackendRunId = activeRun?.remoteRunId ?? null;
|
|
51
|
+
const stopDisabledReason = getStopDisabledReason(activeRun);
|
|
52
|
+
const canStopCurrentRun = Boolean(
|
|
53
|
+
activeRun && (stopDisabledReason === null || (activeRun.remoteStopCapable && !activeBackendRunId))
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const sendMessage = useCallback(async (payload: SendMessageParams) => {
|
|
57
|
+
const requestedSkills = normalizeRequestedSkills(payload.requestedSkills);
|
|
58
|
+
const metadata = buildSendMetadata(payload, requestedSkills);
|
|
59
|
+
await controller.send({
|
|
60
|
+
message: payload.message,
|
|
61
|
+
sessionId: payload.sessionKey,
|
|
62
|
+
agentId: payload.agentId,
|
|
63
|
+
metadata,
|
|
64
|
+
restoreDraftOnError: payload.restoreDraftOnError,
|
|
65
|
+
stopCapable: payload.stopSupported,
|
|
66
|
+
stopReason: payload.stopReason
|
|
67
|
+
});
|
|
68
|
+
}, [controller]);
|
|
69
|
+
|
|
70
|
+
const resumeRun = useCallback(async (run: ChatRunView) => {
|
|
71
|
+
const backendRunId = run.runId?.trim();
|
|
72
|
+
const sessionKey = run.sessionKey?.trim();
|
|
73
|
+
if (!backendRunId || !sessionKey) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const metadata = buildResumeMetadata(run);
|
|
77
|
+
await controller.resume({
|
|
78
|
+
remoteRunId: backendRunId,
|
|
79
|
+
sessionId: sessionKey,
|
|
80
|
+
agentId: run.agentId,
|
|
81
|
+
metadata,
|
|
82
|
+
stopCapable: run.stopSupported,
|
|
83
|
+
stopReason: run.stopReason
|
|
84
|
+
});
|
|
85
|
+
}, [controller]);
|
|
86
|
+
|
|
87
|
+
const stopCurrentRun = useCallback(async () => {
|
|
88
|
+
await controller.stop();
|
|
89
|
+
}, [controller]);
|
|
90
|
+
|
|
91
|
+
const resetStreamState = useCallback(() => {
|
|
92
|
+
activeHistorySessionKeyRef.current = null;
|
|
93
|
+
controller.reset();
|
|
94
|
+
}, [controller]);
|
|
95
|
+
|
|
96
|
+
const applyHistoryMessages = useCallback((messages: SessionMessageView[], options?: { isLoading?: boolean }) => {
|
|
97
|
+
const isRunActive = Boolean(controller.activeRun$.getValue() || controller.isAgentResponding$.getValue());
|
|
98
|
+
if (isRunActive) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const selectedSessionKey = paramsRef.current.selectedSessionKeyRef.current;
|
|
102
|
+
if (selectedSessionKey !== activeHistorySessionKeyRef.current) {
|
|
103
|
+
activeHistorySessionKeyRef.current = selectedSessionKey;
|
|
104
|
+
if (controller.getMessages().length > 0) {
|
|
105
|
+
controller.setMessages([]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!selectedSessionKey) {
|
|
109
|
+
if (controller.getMessages().length > 0) {
|
|
110
|
+
controller.setMessages([]);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (options?.isLoading && messages.length === 0) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
controller.setMessages(buildUiMessagesFromHistoryMessages(messages));
|
|
118
|
+
}, [controller]);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
uiMessages,
|
|
122
|
+
isSending,
|
|
123
|
+
isAwaitingAssistantOutput,
|
|
124
|
+
canStopCurrentRun,
|
|
125
|
+
stopDisabledReason,
|
|
126
|
+
lastSendError,
|
|
127
|
+
activeBackendRunId,
|
|
128
|
+
sendMessage,
|
|
129
|
+
stopCurrentRun,
|
|
130
|
+
resumeRun,
|
|
131
|
+
resetStreamState,
|
|
132
|
+
applyHistoryMessages
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { useEffect, useMemo } from 'react';
|
|
2
|
+
import type { Dispatch, SetStateAction } from 'react';
|
|
3
|
+
import type { SessionEntryView } from '@/api/types';
|
|
4
|
+
import { t } from '@/lib/i18n';
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_SESSION_TYPE = 'native';
|
|
7
|
+
|
|
8
|
+
export type ChatSessionTypeOption = {
|
|
9
|
+
value: string;
|
|
10
|
+
label: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type UseChatSessionTypeStateParams = {
|
|
14
|
+
selectedSession: SessionEntryView | null;
|
|
15
|
+
selectedSessionKey: string | null;
|
|
16
|
+
pendingSessionType: string;
|
|
17
|
+
setPendingSessionType: Dispatch<SetStateAction<string>>;
|
|
18
|
+
sessionTypesData?: {
|
|
19
|
+
defaultType?: string;
|
|
20
|
+
options?: Array<{ value: string; label: string }>;
|
|
21
|
+
} | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function normalizeSessionType(value: unknown): string {
|
|
25
|
+
if (typeof value !== 'string') {
|
|
26
|
+
return DEFAULT_SESSION_TYPE;
|
|
27
|
+
}
|
|
28
|
+
const normalized = value.trim().toLowerCase();
|
|
29
|
+
return normalized || DEFAULT_SESSION_TYPE;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveSessionTypeLabel(sessionType: string, fallbackLabel?: string): string {
|
|
33
|
+
if (sessionType === 'native') {
|
|
34
|
+
return t('chatSessionTypeNative');
|
|
35
|
+
}
|
|
36
|
+
if (sessionType === 'codex-sdk') {
|
|
37
|
+
return t('chatSessionTypeCodex');
|
|
38
|
+
}
|
|
39
|
+
if (sessionType === 'claude-agent-sdk') {
|
|
40
|
+
return t('chatSessionTypeClaude');
|
|
41
|
+
}
|
|
42
|
+
return fallbackLabel?.trim() || sessionType;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildSessionTypeOptions(
|
|
46
|
+
options: Array<{ value: string; label: string }>
|
|
47
|
+
): ChatSessionTypeOption[] {
|
|
48
|
+
const deduped = new Map<string, ChatSessionTypeOption>();
|
|
49
|
+
for (const option of options) {
|
|
50
|
+
const value = normalizeSessionType(option.value);
|
|
51
|
+
deduped.set(value, {
|
|
52
|
+
value,
|
|
53
|
+
label: option.label?.trim() || resolveSessionTypeLabel(value)
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (!deduped.has(DEFAULT_SESSION_TYPE)) {
|
|
57
|
+
deduped.set(DEFAULT_SESSION_TYPE, {
|
|
58
|
+
value: DEFAULT_SESSION_TYPE,
|
|
59
|
+
label: resolveSessionTypeLabel(DEFAULT_SESSION_TYPE)
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return Array.from(deduped.values()).sort((left, right) => {
|
|
63
|
+
if (left.value === DEFAULT_SESSION_TYPE) {
|
|
64
|
+
return -1;
|
|
65
|
+
}
|
|
66
|
+
if (right.value === DEFAULT_SESSION_TYPE) {
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
return left.value.localeCompare(right.value);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function useChatSessionTypeState(params: UseChatSessionTypeStateParams): {
|
|
74
|
+
sessionTypeOptions: ChatSessionTypeOption[];
|
|
75
|
+
defaultSessionType: string;
|
|
76
|
+
selectedSessionType: string;
|
|
77
|
+
canEditSessionType: boolean;
|
|
78
|
+
sessionTypeUnavailable: boolean;
|
|
79
|
+
sessionTypeUnavailableMessage: string | null;
|
|
80
|
+
} {
|
|
81
|
+
const {
|
|
82
|
+
selectedSession,
|
|
83
|
+
selectedSessionKey,
|
|
84
|
+
pendingSessionType,
|
|
85
|
+
setPendingSessionType,
|
|
86
|
+
sessionTypesData
|
|
87
|
+
} = params;
|
|
88
|
+
|
|
89
|
+
const runtimeSessionTypeOptions = useMemo(
|
|
90
|
+
() => buildSessionTypeOptions(sessionTypesData?.options ?? []),
|
|
91
|
+
[sessionTypesData?.options]
|
|
92
|
+
);
|
|
93
|
+
const sessionTypeOptions = useMemo(() => {
|
|
94
|
+
const options = [...runtimeSessionTypeOptions];
|
|
95
|
+
const currentSessionType = normalizeSessionType(selectedSession?.sessionType);
|
|
96
|
+
if (!options.some((option) => option.value === currentSessionType)) {
|
|
97
|
+
options.push({
|
|
98
|
+
value: currentSessionType,
|
|
99
|
+
label: resolveSessionTypeLabel(currentSessionType)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return options.sort((left, right) => {
|
|
103
|
+
if (left.value === DEFAULT_SESSION_TYPE) {
|
|
104
|
+
return -1;
|
|
105
|
+
}
|
|
106
|
+
if (right.value === DEFAULT_SESSION_TYPE) {
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
return left.value.localeCompare(right.value);
|
|
110
|
+
});
|
|
111
|
+
}, [runtimeSessionTypeOptions, selectedSession?.sessionType]);
|
|
112
|
+
const defaultSessionType = useMemo(
|
|
113
|
+
() => normalizeSessionType(sessionTypesData?.defaultType ?? DEFAULT_SESSION_TYPE),
|
|
114
|
+
[sessionTypesData?.defaultType]
|
|
115
|
+
);
|
|
116
|
+
const selectedSessionType = useMemo(
|
|
117
|
+
() => normalizeSessionType(selectedSession?.sessionType ?? pendingSessionType ?? defaultSessionType),
|
|
118
|
+
[defaultSessionType, pendingSessionType, selectedSession?.sessionType]
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (selectedSessionKey) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
setPendingSessionType(defaultSessionType);
|
|
126
|
+
}, [defaultSessionType, selectedSessionKey, setPendingSessionType]);
|
|
127
|
+
|
|
128
|
+
const canEditSessionType = !selectedSessionKey || Boolean(selectedSession?.sessionTypeMutable);
|
|
129
|
+
const availableSessionTypeSet = useMemo(
|
|
130
|
+
() => new Set(runtimeSessionTypeOptions.map((option) => option.value)),
|
|
131
|
+
[runtimeSessionTypeOptions]
|
|
132
|
+
);
|
|
133
|
+
const sessionTypeUnavailable = Boolean(
|
|
134
|
+
selectedSession && !availableSessionTypeSet.has(normalizeSessionType(selectedSession.sessionType))
|
|
135
|
+
);
|
|
136
|
+
const sessionTypeUnavailableMessage = sessionTypeUnavailable
|
|
137
|
+
? `${resolveSessionTypeLabel(selectedSessionType)} ${t('chatSessionTypeUnavailableSuffix')}`
|
|
138
|
+
: null;
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
sessionTypeOptions,
|
|
142
|
+
defaultSessionType,
|
|
143
|
+
selectedSessionType,
|
|
144
|
+
canEditSessionType,
|
|
145
|
+
sessionTypeUnavailable,
|
|
146
|
+
sessionTypeUnavailableMessage
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -9,7 +9,7 @@ interface MaskedInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
|
9
9
|
isSet?: boolean;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function MaskedInput({
|
|
12
|
+
export function MaskedInput({ isSet, className, value, onChange, placeholder, ...props }: MaskedInputProps) {
|
|
13
13
|
const [showKey, setShowKey] = useState(false);
|
|
14
14
|
const [isEditing, setIsEditing] = useState(false);
|
|
15
15
|
const hasUserInput = typeof value === 'string' && value.length > 0;
|
package/src/hooks/useConfig.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
fetchChatRun,
|
|
25
25
|
fetchChatRuns,
|
|
26
26
|
fetchChatCapabilities,
|
|
27
|
+
fetchChatSessionTypes,
|
|
27
28
|
fetchCronJobs,
|
|
28
29
|
deleteCronJob,
|
|
29
30
|
setCronJobEnabled,
|
|
@@ -294,9 +295,26 @@ export function useChatCapabilities(params?: { sessionKey?: string | null; agent
|
|
|
294
295
|
});
|
|
295
296
|
}
|
|
296
297
|
|
|
297
|
-
export function
|
|
298
|
+
export function useChatSessionTypes() {
|
|
299
|
+
return useQuery({
|
|
300
|
+
queryKey: ['chat-session-types'],
|
|
301
|
+
queryFn: fetchChatSessionTypes,
|
|
302
|
+
staleTime: 10_000,
|
|
303
|
+
retry: false
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function useChatRuns(params?: {
|
|
308
|
+
sessionKey?: string | null;
|
|
309
|
+
states?: Array<'queued' | 'running' | 'completed' | 'failed' | 'aborted'>;
|
|
310
|
+
limit?: number;
|
|
311
|
+
syncActiveStates?: boolean;
|
|
312
|
+
isLocallyRunning?: boolean;
|
|
313
|
+
}) {
|
|
298
314
|
const sessionKey = params?.sessionKey?.trim() || undefined;
|
|
299
315
|
const states = Array.isArray(params?.states) && params.states.length > 0 ? params.states : undefined;
|
|
316
|
+
const isActiveStatesQuery = Boolean(states?.some((state) => state === 'queued' || state === 'running'));
|
|
317
|
+
const shouldSyncActiveStates = Boolean(params?.syncActiveStates && isActiveStatesQuery);
|
|
300
318
|
return useQuery({
|
|
301
319
|
queryKey: ['chat-runs', sessionKey ?? null, states ?? null, params?.limit ?? null],
|
|
302
320
|
queryFn: () => fetchChatRuns({
|
|
@@ -306,6 +324,18 @@ export function useChatRuns(params?: { sessionKey?: string | null; states?: Arra
|
|
|
306
324
|
}),
|
|
307
325
|
enabled: Boolean(sessionKey) || Boolean(states),
|
|
308
326
|
staleTime: 5_000,
|
|
327
|
+
refetchInterval: (query) => {
|
|
328
|
+
if (!shouldSyncActiveStates) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
if (params?.isLocallyRunning) {
|
|
332
|
+
return 800;
|
|
333
|
+
}
|
|
334
|
+
const data = query.state.data;
|
|
335
|
+
const hasActiveRuns = Array.isArray(data?.runs) && data.runs.length > 0;
|
|
336
|
+
return hasActiveRuns ? 800 : false;
|
|
337
|
+
},
|
|
338
|
+
refetchIntervalInBackground: false,
|
|
309
339
|
retry: false
|
|
310
340
|
});
|
|
311
341
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import type { BehaviorSubject, Observable } from 'rxjs';
|
|
3
|
+
|
|
4
|
+
export function useValueFromBehaviorSubject<T>(subject: BehaviorSubject<T>): T {
|
|
5
|
+
const [state, setState] = useState<T>(subject.getValue());
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const subscription = subject.subscribe(setState);
|
|
8
|
+
return () => subscription.unsubscribe();
|
|
9
|
+
}, [subject]);
|
|
10
|
+
return state;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useValueFromObservable<T>(observable: Observable<T>, defaultValue: T): T {
|
|
14
|
+
const [state, setState] = useState<T>(defaultValue);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const subscription = observable.subscribe(setState);
|
|
17
|
+
return () => subscription.unsubscribe();
|
|
18
|
+
}, [observable]);
|
|
19
|
+
return state;
|
|
20
|
+
}
|
package/src/lib/chat-message.ts
CHANGED
|
@@ -11,37 +11,6 @@ export type ToolCard = {
|
|
|
11
11
|
hasResult?: boolean;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
export type ChatTimelineMessageItem = {
|
|
15
|
-
kind: 'message';
|
|
16
|
-
key: string;
|
|
17
|
-
role: ChatRole;
|
|
18
|
-
timestamp: string;
|
|
19
|
-
message: SessionMessageView;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export type ChatTimelineAssistantTurnSegment =
|
|
23
|
-
| {
|
|
24
|
-
kind: 'assistant_message';
|
|
25
|
-
key: string;
|
|
26
|
-
text: string;
|
|
27
|
-
reasoning: string;
|
|
28
|
-
}
|
|
29
|
-
| {
|
|
30
|
-
kind: 'tool_card';
|
|
31
|
-
key: string;
|
|
32
|
-
card: ToolCard;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export type ChatTimelineAssistantTurnItem = {
|
|
36
|
-
kind: 'assistant_turn';
|
|
37
|
-
key: string;
|
|
38
|
-
role: 'assistant';
|
|
39
|
-
timestamp: string;
|
|
40
|
-
segments: ChatTimelineAssistantTurnSegment[];
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export type ChatTimelineItem = ChatTimelineMessageItem | ChatTimelineAssistantTurnItem;
|
|
44
|
-
|
|
45
14
|
const TOOL_DETAIL_FIELDS = ['cmd', 'command', 'query', 'q', 'path', 'url', 'to', 'channel', 'agentId', 'sessionKey'];
|
|
46
15
|
|
|
47
16
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -55,7 +24,7 @@ function truncateText(value: string, maxChars = 2400): string {
|
|
|
55
24
|
return `${value.slice(0, maxChars)}\n…`;
|
|
56
25
|
}
|
|
57
26
|
|
|
58
|
-
function stringifyUnknown(value: unknown): string {
|
|
27
|
+
export function stringifyUnknown(value: unknown): string {
|
|
59
28
|
if (typeof value === 'string') {
|
|
60
29
|
return value;
|
|
61
30
|
}
|
|
@@ -91,7 +60,7 @@ function parseArgsObject(value: unknown): Record<string, unknown> | null {
|
|
|
91
60
|
}
|
|
92
61
|
}
|
|
93
62
|
|
|
94
|
-
function summarizeToolArgs(args: unknown): string | undefined {
|
|
63
|
+
export function summarizeToolArgs(args: unknown): string | undefined {
|
|
95
64
|
const parsed = parseArgsObject(args);
|
|
96
65
|
if (!parsed) {
|
|
97
66
|
const text = stringifyUnknown(args).trim();
|
|
@@ -212,20 +181,6 @@ export function extractToolCards(message: SessionMessageView): ToolCard[] {
|
|
|
212
181
|
return cards;
|
|
213
182
|
}
|
|
214
183
|
|
|
215
|
-
function normalizeEvent(event: SessionEventView, index: number): SessionEventView & { _idx: number; _seq: number } {
|
|
216
|
-
const seq = Number.isFinite(event.seq) && event.seq > 0 ? Math.trunc(event.seq) : index + 1;
|
|
217
|
-
const timestamp =
|
|
218
|
-
typeof event.timestamp === 'string' && event.timestamp
|
|
219
|
-
? event.timestamp
|
|
220
|
-
: event.message?.timestamp ?? new Date().toISOString();
|
|
221
|
-
return {
|
|
222
|
-
...event,
|
|
223
|
-
timestamp,
|
|
224
|
-
_idx: index,
|
|
225
|
-
_seq: seq
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
|
|
229
184
|
function inferEventTypeFromMessage(message: SessionMessageView): string {
|
|
230
185
|
const role = normalizeChatRole(message);
|
|
231
186
|
if (role === 'assistant' && hasToolCalls(message)) {
|
|
@@ -245,158 +200,3 @@ export function buildFallbackEventsFromMessages(messages: SessionMessageView[]):
|
|
|
245
200
|
message
|
|
246
201
|
}));
|
|
247
202
|
}
|
|
248
|
-
|
|
249
|
-
function appendText(base: string, next: string): string {
|
|
250
|
-
if (!next) {
|
|
251
|
-
return base;
|
|
252
|
-
}
|
|
253
|
-
if (!base) {
|
|
254
|
-
return next;
|
|
255
|
-
}
|
|
256
|
-
return `${base}\n\n${next}`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export function buildChatTimeline(events: SessionEventView[]): ChatTimelineItem[] {
|
|
260
|
-
const normalized = events
|
|
261
|
-
.map((event, index) => normalizeEvent(event, index))
|
|
262
|
-
.sort((left, right) => {
|
|
263
|
-
if (left._seq !== right._seq) {
|
|
264
|
-
return left._seq - right._seq;
|
|
265
|
-
}
|
|
266
|
-
const leftTs = Date.parse(left.timestamp);
|
|
267
|
-
const rightTs = Date.parse(right.timestamp);
|
|
268
|
-
if (Number.isFinite(leftTs) && Number.isFinite(rightTs) && leftTs !== rightTs) {
|
|
269
|
-
return leftTs - rightTs;
|
|
270
|
-
}
|
|
271
|
-
return left._idx - right._idx;
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
const timeline: ChatTimelineItem[] = [];
|
|
275
|
-
let activeTurn:
|
|
276
|
-
| {
|
|
277
|
-
item: ChatTimelineAssistantTurnItem;
|
|
278
|
-
cardByCallId: Map<string, ToolCard>;
|
|
279
|
-
}
|
|
280
|
-
| null = null;
|
|
281
|
-
|
|
282
|
-
const closeActiveTurn = () => {
|
|
283
|
-
activeTurn = null;
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
const ensureActiveTurn = (eventKey: string, timestamp: string) => {
|
|
287
|
-
if (activeTurn) {
|
|
288
|
-
activeTurn.item.timestamp = timestamp;
|
|
289
|
-
return activeTurn;
|
|
290
|
-
}
|
|
291
|
-
const item: ChatTimelineAssistantTurnItem = {
|
|
292
|
-
kind: 'assistant_turn',
|
|
293
|
-
key: `turn-${eventKey}`,
|
|
294
|
-
role: 'assistant',
|
|
295
|
-
timestamp,
|
|
296
|
-
segments: []
|
|
297
|
-
};
|
|
298
|
-
timeline.push(item);
|
|
299
|
-
activeTurn = {
|
|
300
|
-
item,
|
|
301
|
-
cardByCallId: new Map<string, ToolCard>()
|
|
302
|
-
};
|
|
303
|
-
return activeTurn;
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
const pushAssistantMessageSegment = (
|
|
307
|
-
target: { item: ChatTimelineAssistantTurnItem },
|
|
308
|
-
eventKey: string,
|
|
309
|
-
message: SessionMessageView
|
|
310
|
-
) => {
|
|
311
|
-
const text = extractMessageText(message.content).trim();
|
|
312
|
-
const reasoning =
|
|
313
|
-
typeof message.reasoning_content === 'string' ? message.reasoning_content.trim() : '';
|
|
314
|
-
if (!text && !reasoning) {
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
target.item.segments.push({
|
|
318
|
-
kind: 'assistant_message',
|
|
319
|
-
key: `assistant-${eventKey}-${target.item.segments.length}`,
|
|
320
|
-
text,
|
|
321
|
-
reasoning
|
|
322
|
-
});
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
for (const event of normalized) {
|
|
326
|
-
const message = event.message;
|
|
327
|
-
if (!message) {
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const role = normalizeChatRole(message);
|
|
332
|
-
const timestamp =
|
|
333
|
-
typeof message.timestamp === 'string' && message.timestamp
|
|
334
|
-
? message.timestamp
|
|
335
|
-
: event.timestamp;
|
|
336
|
-
const eventKey = `${event._seq}-${event._idx}`;
|
|
337
|
-
|
|
338
|
-
if (role === 'assistant') {
|
|
339
|
-
const turn = ensureActiveTurn(eventKey, timestamp);
|
|
340
|
-
pushAssistantMessageSegment(turn, eventKey, message);
|
|
341
|
-
if (!hasToolCalls(message)) {
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const toolCards = buildToolCallCards(message);
|
|
346
|
-
for (const card of toolCards) {
|
|
347
|
-
turn.item.segments.push({
|
|
348
|
-
kind: 'tool_card',
|
|
349
|
-
key: `tool-call-${eventKey}-${turn.item.segments.length}`,
|
|
350
|
-
card
|
|
351
|
-
});
|
|
352
|
-
if (typeof card.callId === 'string' && card.callId.trim()) {
|
|
353
|
-
turn.cardByCallId.set(card.callId, card);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (role === 'tool') {
|
|
360
|
-
const turn = ensureActiveTurn(eventKey, timestamp);
|
|
361
|
-
const callId =
|
|
362
|
-
typeof message.tool_call_id === 'string' && message.tool_call_id.trim()
|
|
363
|
-
? message.tool_call_id.trim()
|
|
364
|
-
: undefined;
|
|
365
|
-
if (callId && turn.cardByCallId.has(callId)) {
|
|
366
|
-
const card = turn.cardByCallId.get(callId)!;
|
|
367
|
-
const resultText = extractMessageText(message.content).trim();
|
|
368
|
-
card.text = appendText(card.text ?? '', resultText);
|
|
369
|
-
card.hasResult = true;
|
|
370
|
-
if (typeof message.name === 'string' && message.name.trim()) {
|
|
371
|
-
card.name = message.name.trim();
|
|
372
|
-
}
|
|
373
|
-
turn.item.timestamp = timestamp;
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
turn.item.segments.push({
|
|
378
|
-
kind: 'tool_card',
|
|
379
|
-
key: `tool-result-${eventKey}-${turn.item.segments.length}`,
|
|
380
|
-
card: {
|
|
381
|
-
kind: 'result',
|
|
382
|
-
name: toToolName(message.name),
|
|
383
|
-
text: extractMessageText(message.content).trim(),
|
|
384
|
-
callId,
|
|
385
|
-
hasResult: true
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
timeline.push({
|
|
392
|
-
kind: 'message',
|
|
393
|
-
key: `message-${event._seq}-${event._idx}`,
|
|
394
|
-
role,
|
|
395
|
-
timestamp,
|
|
396
|
-
message
|
|
397
|
-
});
|
|
398
|
-
closeActiveTurn();
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return timeline;
|
|
402
|
-
}
|