@nextclaw/ui 0.14.0 → 0.14.1-beta.0
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 +23 -0
- package/dist/assets/{api-B50yVdT8.js → api-C6EDHBDR.js} +1 -1
- package/dist/assets/channels-list-page-DKj6m1Hc.js +8 -0
- package/dist/assets/chat-page-BXngLJY6.js +105 -0
- package/dist/assets/config-split-page-CjlmJy8X.js +1 -0
- package/dist/assets/{confirm-dialog-DRSelLdI.js → confirm-dialog-B39X0pYp.js} +1 -1
- package/dist/assets/desktop-update-config-DfUQ8nly.js +1 -0
- package/dist/assets/{dist-NZRV-BxD.js → dist-BIYV2-Ia.js} +1 -1
- package/dist/assets/{dist-D19wiQwB.js → dist-CmMro3nV.js} +1 -1
- package/dist/assets/doc-browser-B6Wc_TIY.js +1 -0
- package/dist/assets/doc-browser-CDtUtcim.js +1 -0
- package/dist/assets/{doc-browser-context-C8KPbj-p.js → doc-browser-context-DZbC9zVB.js} +1 -1
- package/dist/assets/{ellipsis-BYy8TP0A.js → ellipsis-DDpgTzgx.js} +1 -1
- package/dist/assets/{external-link-528k6MDD.js → external-link-D2cBOCqn.js} +1 -1
- package/dist/assets/index-DTb_XVIH.js +103 -0
- package/dist/assets/index-ayiPYhf1.css +1 -0
- package/dist/assets/mcp-marketplace-page-DZ9H0v05.js +1 -0
- package/dist/assets/mcp-marketplace-page-cnVWf7QQ.js +40 -0
- package/dist/assets/model-config-page-Bcb35AVv.js +1 -0
- package/dist/assets/plus-CYE1onU2.js +1 -0
- package/dist/assets/provider-scoped-model-input-kdoNp4HG.js +1 -0
- package/dist/assets/providers-config-page-CQSibSwO.js +1 -0
- package/dist/assets/{react-Dsotw5vM.js → react-DYQXXGbH.js} +1 -1
- package/dist/assets/remote-BlJDbXOI.js +1 -0
- package/dist/assets/rotate-cw-BWbvFDAX.js +1 -0
- package/dist/assets/runtime-config-page-Tn2yLQ0c.js +1 -0
- package/dist/assets/{save-CHDnDTok.js → save-BNVTgBVn.js} +1 -1
- package/dist/assets/search-CKCzwLId.js +1 -0
- package/dist/assets/search-config-page-DG5Q2hF1.js +1 -0
- package/dist/assets/{secrets-config-page-95yaH1fa.js → secrets-config-page-BXn5Uuwp.js} +2 -2
- package/dist/assets/select-CIRwTNP4.js +41 -0
- package/dist/assets/{tag-chip-Cr8GwpRE.js → tag-chip-DKhVFtTE.js} +1 -1
- package/dist/assets/{use-config-TzwzTgg0.js → use-config-DgKM9VIU.js} +1 -1
- package/dist/assets/x-Cm3JjYut.js +1 -0
- package/dist/index.html +14 -27
- package/package.json +9 -9
- package/src/app/presenters/app.presenter.ts +2 -0
- package/src/features/account/components/account-panel.tsx +2 -2
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +1 -0
- package/src/features/chat/features/input/components/chat-input-bar.container.tsx +39 -80
- package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +118 -0
- package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +2 -17
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -24
- package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +0 -27
- package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +89 -0
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +9 -1
- package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +115 -0
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +4 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +119 -1
- package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +51 -0
- package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +28 -5
- package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +4 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-ui-show-content-event.test.tsx +1 -0
- package/src/features/chat/features/ncp/utils/ncp-chat-query-derived.utils.ts +11 -3
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +27 -0
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +19 -1
- package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +46 -0
- package/src/features/chat/features/session/hooks/__tests__/use-selected-session-context-window-indicator.test.tsx +63 -0
- package/src/features/chat/features/session/hooks/use-selected-session-context-window-indicator.ts +7 -10
- package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +8 -0
- package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +2 -0
- package/src/features/chat/features/session-type/hooks/use-chat-session-type-state.ts +1 -0
- package/src/features/chat/features/session-type/utils/chat-session-type.utils.ts +4 -0
- package/src/features/chat/index.ts +1 -0
- package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +28 -0
- package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +129 -0
- package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +34 -0
- package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +143 -1
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +30 -0
- package/src/features/chat/managers/__tests__/recent-selection.manager.test.ts +37 -14
- package/src/features/chat/managers/chat-draft-intent.manager.ts +46 -0
- package/src/features/chat/managers/chat-input.manager.ts +13 -13
- package/src/features/chat/managers/chat-run.manager.ts +3 -0
- package/src/features/chat/managers/chat-session-preference-sync.manager.ts +19 -2
- package/src/features/chat/managers/chat-thread.manager.ts +4 -0
- package/src/features/chat/managers/chat-ui.manager.ts +2 -6
- package/src/features/chat/managers/recent-selection.manager.ts +21 -67
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +47 -1
- package/src/features/chat/pages/ncp-chat-page.tsx +18 -0
- package/src/features/chat/stores/chat-input.store.ts +7 -1
- package/src/features/chat/stores/recent-selection.store.ts +85 -0
- package/src/features/chat/types/chat-input-bar.types.ts +0 -1
- package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +137 -0
- package/src/features/panel-apps/components/panel-apps-list.tsx +119 -5
- package/src/features/panel-apps/index.ts +2 -0
- package/src/features/panel-apps/utils/panel-app-entry-match.utils.ts +17 -0
- package/src/shared/lib/api/chat-session-type.types.ts +7 -0
- package/src/shared/lib/api/ncp-session.types.ts +1 -0
- package/src/shared/lib/api/types.ts +3 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +4 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +10 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +4 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +10 -0
- package/dist/assets/app-presenter-provider-BI-ewLbO.js +0 -3
- package/dist/assets/arrow-left-BOCJyLbQ.js +0 -1
- package/dist/assets/book-open-Bd2Xbrrc.js +0 -1
- package/dist/assets/channels-list-page-moJkpxtU.js +0 -8
- package/dist/assets/chat-page-CO27cl4v.js +0 -105
- package/dist/assets/config-split-page-CsQ2rIdq.js +0 -1
- package/dist/assets/desktop-update-config-qa-SynOc.js +0 -1
- package/dist/assets/doc-browser-2ddUJCA0.js +0 -1
- package/dist/assets/doc-browser-BjHUczQb.js +0 -1
- package/dist/assets/es2015-BUIIKCNh.js +0 -41
- package/dist/assets/host-capabilities-9bB_998Z.js +0 -1
- package/dist/assets/index-CMNrRhnG.js +0 -101
- package/dist/assets/index-UTmTW1eU.css +0 -1
- package/dist/assets/key-round-D3vD9dj7.js +0 -1
- package/dist/assets/loader-circle-DV9TSRns.js +0 -1
- package/dist/assets/mcp-marketplace-page-CS5Lh3oA.js +0 -1
- package/dist/assets/mcp-marketplace-page-DEOMcWb0.js +0 -40
- package/dist/assets/model-config-page-BSc4o7pb.js +0 -1
- package/dist/assets/notice-card-qqlbsOoR.js +0 -1
- package/dist/assets/plus-Bf9qhPDO.js +0 -1
- package/dist/assets/popover-BnmPOXe3.js +0 -1
- package/dist/assets/provider-scoped-model-input-Ca56s475.js +0 -1
- package/dist/assets/providers-config-page-D8Kf4U3u.js +0 -1
- package/dist/assets/refresh-cw-Bmx4wc9P.js +0 -1
- package/dist/assets/remote-DNMEy_c4.js +0 -1
- package/dist/assets/rotate-cw-CL0sVBel.js +0 -1
- package/dist/assets/runtime-config-page-CUFtKr0U.js +0 -1
- package/dist/assets/search-D7L-TCH-.js +0 -1
- package/dist/assets/search-config-page-CA3RVAef.js +0 -1
- package/dist/assets/status-dot-Dm4h-bjn.js +0 -1
- package/dist/assets/tabs-CJvw5Lqp.js +0 -1
- package/dist/assets/tooltip-Bxzi6aDL.js +0 -1
- package/dist/assets/trash-2-CSfX-BvU.js +0 -1
- package/dist/assets/x-fkIWvi07.js +0 -1
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-bar-controller.test.tsx +0 -152
- package/src/features/chat/features/input/hooks/use-chat-input-bar-controller.ts +0 -142
- /package/dist/assets/{config-hints-Cl2XAhzV.js → config-hints-CTqBnCDp.js} +0 -0
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
buildChatRunMetadata,
|
|
12
12
|
shouldClearPendingProjectRootOverride
|
|
13
13
|
} from '@/features/chat/features/session/utils/chat-run-metadata.utils';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
filterNcpChatModelOptionsBySessionType,
|
|
16
|
+
} from '@/features/chat/features/ncp/utils/ncp-chat-query-derived.utils';
|
|
15
17
|
|
|
16
18
|
const modelOptions: ChatModelOption[] = [
|
|
17
19
|
{
|
|
@@ -83,6 +85,29 @@ describe('filterNcpChatModelOptionsBySessionType', () => {
|
|
|
83
85
|
}]);
|
|
84
86
|
});
|
|
85
87
|
|
|
88
|
+
it('attaches runtime default thinking capability when the runtime publishes one', () => {
|
|
89
|
+
expect(
|
|
90
|
+
filterNcpChatModelOptionsBySessionType({
|
|
91
|
+
modelOptions,
|
|
92
|
+
modelSelectionMode: 'runtime-default',
|
|
93
|
+
runtimeDefaultModelLabel: 'Runtime default',
|
|
94
|
+
runtimeDefaultThinkingCapability: {
|
|
95
|
+
supported: ['minimal', 'low', 'medium', 'high'],
|
|
96
|
+
default: 'high',
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
).toEqual([{
|
|
100
|
+
value: RUNTIME_DEFAULT_MODEL_VALUE,
|
|
101
|
+
modelLabel: 'Runtime default',
|
|
102
|
+
providerLabel: '',
|
|
103
|
+
isRuntimeDefault: true,
|
|
104
|
+
thinkingCapability: {
|
|
105
|
+
supported: ['minimal', 'low', 'medium', 'high'],
|
|
106
|
+
default: 'high',
|
|
107
|
+
},
|
|
108
|
+
}]);
|
|
109
|
+
});
|
|
110
|
+
|
|
86
111
|
it('adds the runtime default option beside supported models for optional runtime selection', () => {
|
|
87
112
|
expect(
|
|
88
113
|
filterNcpChatModelOptionsBySessionType({
|
|
@@ -102,6 +127,27 @@ describe('filterNcpChatModelOptionsBySessionType', () => {
|
|
|
102
127
|
modelOptions[1],
|
|
103
128
|
]);
|
|
104
129
|
});
|
|
130
|
+
|
|
131
|
+
it('adds runtime default thinking capability beside supported models for optional runtime selection', () => {
|
|
132
|
+
expect(
|
|
133
|
+
filterNcpChatModelOptionsBySessionType({
|
|
134
|
+
modelOptions,
|
|
135
|
+
modelSelectionMode: 'optional',
|
|
136
|
+
runtimeDefaultModelLabel: 'Runtime default',
|
|
137
|
+
runtimeDefaultThinkingCapability: {
|
|
138
|
+
supported: ['minimal', 'low', 'medium', 'high'],
|
|
139
|
+
default: 'high',
|
|
140
|
+
},
|
|
141
|
+
supportedModels: ['openai/gpt-5'],
|
|
142
|
+
})[0]
|
|
143
|
+
).toMatchObject({
|
|
144
|
+
value: RUNTIME_DEFAULT_MODEL_VALUE,
|
|
145
|
+
thinkingCapability: {
|
|
146
|
+
supported: ['minimal', 'low', 'medium', 'high'],
|
|
147
|
+
default: 'high',
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
});
|
|
105
151
|
});
|
|
106
152
|
|
|
107
153
|
describe('buildChatRunMetadata', () => {
|
|
@@ -30,6 +30,23 @@ import { isNcpChatRuntimeBlocked, resolveNcpChatSendErrorMessage } from "@/featu
|
|
|
30
30
|
import { useUiShowContentEvent } from "@/features/chat/features/ncp/hooks/use-ui-show-content-event";
|
|
31
31
|
import { readNcpContextWindowValue } from "@/features/chat/features/session/utils/ncp-session-context-metadata.utils";
|
|
32
32
|
|
|
33
|
+
function useChatDraftIntentConsumer() {
|
|
34
|
+
const appPresenter = useAppPresenter();
|
|
35
|
+
const presenter = usePresenter();
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const applyIntent = (intent: { id: number; prompt: string }) => {
|
|
38
|
+
presenter.startAgentCreationDraft(intent.prompt);
|
|
39
|
+
appPresenter.chatDraftIntentManager.markConsumed(intent.id);
|
|
40
|
+
};
|
|
41
|
+
const unsubscribe = appPresenter.chatDraftIntentManager.subscribe(applyIntent);
|
|
42
|
+
const pendingIntent = appPresenter.chatDraftIntentManager.consumePending();
|
|
43
|
+
if (pendingIntent) {
|
|
44
|
+
applyIntent(pendingIntent);
|
|
45
|
+
}
|
|
46
|
+
return unsubscribe;
|
|
47
|
+
}, [appPresenter, presenter]);
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
function useNcpChatRouteSelection() {
|
|
34
51
|
const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
|
|
35
52
|
const routeSessionKey = useMemo(
|
|
@@ -170,6 +187,7 @@ function NcpChatPageContent({ view }: ChatPageProps) {
|
|
|
170
187
|
syncRouteSessionSelection:
|
|
171
188
|
presenter.chatSessionListManager.syncRouteSessionSelection,
|
|
172
189
|
});
|
|
190
|
+
useChatDraftIntentConsumer();
|
|
173
191
|
useUiShowContentEvent();
|
|
174
192
|
useChatRunSnapshotSync({
|
|
175
193
|
agent,
|
|
@@ -2,7 +2,12 @@ import { create } from 'zustand';
|
|
|
2
2
|
import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
|
|
3
3
|
import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
|
|
4
4
|
import type { RuntimeModelSelectionMode } from '@nextclaw/shared';
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
RuntimeDefaultThinkingView,
|
|
7
|
+
SessionSkillEntryView,
|
|
8
|
+
SessionTypeIconView,
|
|
9
|
+
ThinkingLevel,
|
|
10
|
+
} from '@/shared/lib/api';
|
|
6
11
|
import type { ChatModelOption } from '@/features/chat/types/chat-input.types';
|
|
7
12
|
import { createInitialChatComposerNodes } from '@/features/chat/features/input/utils/chat-composer-state.utils';
|
|
8
13
|
|
|
@@ -32,6 +37,7 @@ export type ChatInputSnapshot = {
|
|
|
32
37
|
supportedModels?: string[];
|
|
33
38
|
recommendedModel?: string | null;
|
|
34
39
|
modelSelectionMode?: RuntimeModelSelectionMode;
|
|
40
|
+
runtimeDefaultThinking?: RuntimeDefaultThinkingView | null;
|
|
35
41
|
cta?: {
|
|
36
42
|
kind: string;
|
|
37
43
|
label?: string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { createJSONStorage, persist } from 'zustand/middleware';
|
|
3
|
+
|
|
4
|
+
export type RecentSelectionScope = {
|
|
5
|
+
namespace?: string | null;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type RecentSelectionStoreState = {
|
|
9
|
+
entriesByKey: Record<string, string[]>;
|
|
10
|
+
setEntries: (key: string, values: string[]) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const RECENT_SELECTION_STORE_KEY = 'nextclaw.chat.recent-selection.store';
|
|
14
|
+
|
|
15
|
+
export function normalizeRecentSelectionValue(value: unknown): string | null {
|
|
16
|
+
if (typeof value !== 'string') {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const normalized = value.trim();
|
|
20
|
+
return normalized.length > 0 ? normalized : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function normalizeRecentSelectionList(values: unknown, limit: number): string[] {
|
|
24
|
+
if (!Array.isArray(values)) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
return Array.from(
|
|
28
|
+
new Set(values.map(normalizeRecentSelectionValue).filter((value): value is string => Boolean(value))),
|
|
29
|
+
).slice(0, limit);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveRecentSelectionKey(
|
|
33
|
+
options: { storageKey: string },
|
|
34
|
+
scope?: RecentSelectionScope,
|
|
35
|
+
): string {
|
|
36
|
+
const namespace = normalizeRecentSelectionValue(scope?.namespace);
|
|
37
|
+
return namespace
|
|
38
|
+
? `${options.storageKey}.${encodeURIComponent(namespace.toLowerCase())}`
|
|
39
|
+
: options.storageKey;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizePersistedEntries(value: unknown): Record<string, string[]> {
|
|
43
|
+
if (!value || typeof value !== 'object') {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
const entries: Record<string, string[]> = {};
|
|
47
|
+
for (const [key, values] of Object.entries(value)) {
|
|
48
|
+
const normalizedKey = normalizeRecentSelectionValue(key);
|
|
49
|
+
if (!normalizedKey) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const normalizedValues = normalizeRecentSelectionList(values, Number.POSITIVE_INFINITY);
|
|
53
|
+
if (normalizedValues.length > 0) {
|
|
54
|
+
entries[normalizedKey] = normalizedValues;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return entries;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const useRecentSelectionStore = create<RecentSelectionStoreState>()(
|
|
61
|
+
persist(
|
|
62
|
+
(set) => ({
|
|
63
|
+
entriesByKey: {},
|
|
64
|
+
setEntries: (key, values) => set((state) => ({
|
|
65
|
+
entriesByKey: {
|
|
66
|
+
...state.entriesByKey,
|
|
67
|
+
[key]: normalizeRecentSelectionList(values, Number.POSITIVE_INFINITY),
|
|
68
|
+
},
|
|
69
|
+
})),
|
|
70
|
+
}),
|
|
71
|
+
{
|
|
72
|
+
name: RECENT_SELECTION_STORE_KEY,
|
|
73
|
+
storage: createJSONStorage(() => window.localStorage),
|
|
74
|
+
partialize: (state) => ({
|
|
75
|
+
entriesByKey: state.entriesByKey,
|
|
76
|
+
}),
|
|
77
|
+
merge: (persistedState, currentState) => ({
|
|
78
|
+
...currentState,
|
|
79
|
+
entriesByKey: normalizePersistedEntries(
|
|
80
|
+
(persistedState as { entriesByKey?: Record<string, unknown> } | undefined)?.entriesByKey,
|
|
81
|
+
),
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
),
|
|
85
|
+
);
|
|
@@ -37,7 +37,6 @@ export type ChatInputBarAdapterTexts = {
|
|
|
37
37
|
modelNoOptionsLabel: string;
|
|
38
38
|
recentModelsLabel: string;
|
|
39
39
|
allModelsLabel: string;
|
|
40
|
-
sessionTypePlaceholder: string;
|
|
41
40
|
thinkingLabels: Record<ChatThinkingLevel, string>;
|
|
42
41
|
noModelOptionsLabel: string;
|
|
43
42
|
configureProviderLabel: string;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { PanelAppsList } from '@/features/panel-apps/components/panel-apps-list';
|
|
5
|
+
|
|
6
|
+
const mocks = vi.hoisted(() => ({
|
|
7
|
+
entries: [] as Array<{
|
|
8
|
+
appId: string;
|
|
9
|
+
clientDeclared: boolean;
|
|
10
|
+
clientGranted: boolean;
|
|
11
|
+
contentPath: string;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
favorite: boolean;
|
|
15
|
+
fileName: string;
|
|
16
|
+
id: string;
|
|
17
|
+
kind: 'single-file';
|
|
18
|
+
openCount: number;
|
|
19
|
+
sizeBytes: number;
|
|
20
|
+
title: string;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
}>,
|
|
23
|
+
navigate: vi.fn(),
|
|
24
|
+
refetchPanelApps: vi.fn(),
|
|
25
|
+
requestDraft: vi.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock('@/app/components/app-presenter-provider', () => ({
|
|
29
|
+
useAppPresenter: () => ({
|
|
30
|
+
chatDraftIntentManager: {
|
|
31
|
+
requestDraft: mocks.requestDraft,
|
|
32
|
+
},
|
|
33
|
+
serviceActionAuthorizationManager: {
|
|
34
|
+
requestAuthorization: vi.fn(async () => true),
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
vi.mock('react-router-dom', async () => ({
|
|
40
|
+
...(await vi.importActual('react-router-dom') as object),
|
|
41
|
+
useNavigate: () => mocks.navigate,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
vi.mock('@/features/panel-apps/hooks/use-panel-apps', () => ({
|
|
45
|
+
useDeletePanelApp: () => ({
|
|
46
|
+
isPending: false,
|
|
47
|
+
mutate: vi.fn(),
|
|
48
|
+
}),
|
|
49
|
+
useGrantPanelAppClient: () => ({
|
|
50
|
+
mutateAsync: vi.fn(),
|
|
51
|
+
}),
|
|
52
|
+
usePanelApps: () => ({
|
|
53
|
+
data: {
|
|
54
|
+
entries: mocks.entries,
|
|
55
|
+
panelsPath: '/workspace/panels',
|
|
56
|
+
workspacePath: '/workspace',
|
|
57
|
+
},
|
|
58
|
+
isError: false,
|
|
59
|
+
isLoading: false,
|
|
60
|
+
refetch: mocks.refetchPanelApps,
|
|
61
|
+
}),
|
|
62
|
+
useRecordPanelAppOpened: () => ({
|
|
63
|
+
mutateAsync: vi.fn(async (id: string) => mocks.entries.find((entry) => entry.id === id)),
|
|
64
|
+
}),
|
|
65
|
+
useUpdatePanelAppPreferences: () => ({
|
|
66
|
+
isPending: false,
|
|
67
|
+
mutate: vi.fn(),
|
|
68
|
+
}),
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
function createPanelAppEntry(overrides: Partial<(typeof mocks.entries)[number]> = {}) {
|
|
72
|
+
return {
|
|
73
|
+
appId: 'demo',
|
|
74
|
+
clientDeclared: false,
|
|
75
|
+
clientGranted: false,
|
|
76
|
+
contentPath: '/api/panel-apps/demo/content',
|
|
77
|
+
createdAt: '2026-05-28T08:00:00.000Z',
|
|
78
|
+
description: 'A compact demo panel.',
|
|
79
|
+
favorite: false,
|
|
80
|
+
fileName: 'demo.panel.html',
|
|
81
|
+
id: 'demo',
|
|
82
|
+
kind: 'single-file' as const,
|
|
83
|
+
openCount: 0,
|
|
84
|
+
sizeBytes: 12,
|
|
85
|
+
title: 'Demo Panel',
|
|
86
|
+
updatedAt: '2026-05-28T09:00:00.000Z',
|
|
87
|
+
...overrides,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe('PanelAppsList', () => {
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
mocks.entries = [];
|
|
94
|
+
mocks.navigate.mockReset();
|
|
95
|
+
mocks.refetchPanelApps.mockReset();
|
|
96
|
+
mocks.requestDraft.mockReset();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('shows first-use guidance when no panel apps exist', async () => {
|
|
100
|
+
const user = userEvent.setup();
|
|
101
|
+
|
|
102
|
+
render(<PanelAppsList onOpenPanelApp={vi.fn()} />);
|
|
103
|
+
|
|
104
|
+
expect(screen.getByText('Create your first panel app')).toBeTruthy();
|
|
105
|
+
expect(screen.getByText('Ask NextClaw to generate one')).toBeTruthy();
|
|
106
|
+
expect(screen.getByText('/workspace/panels')).toBeTruthy();
|
|
107
|
+
|
|
108
|
+
await user.click(screen.getAllByRole('button', { name: 'Refresh panel apps' }).at(-1)!);
|
|
109
|
+
|
|
110
|
+
expect(mocks.refetchPanelApps).toHaveBeenCalledTimes(1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('fills a sample panel app prompt from first-use guidance', async () => {
|
|
114
|
+
const user = userEvent.setup();
|
|
115
|
+
|
|
116
|
+
render(<PanelAppsList onOpenPanelApp={vi.fn()} />);
|
|
117
|
+
|
|
118
|
+
await user.click(screen.getByRole('button', { name: /Create sample panel app/ }));
|
|
119
|
+
|
|
120
|
+
expect(mocks.requestDraft).toHaveBeenCalledWith(expect.stringContaining('personal task board'));
|
|
121
|
+
expect(mocks.navigate).toHaveBeenCalledWith('/chat');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('shows a filtered empty state instead of first-use guidance when apps exist', async () => {
|
|
125
|
+
const user = userEvent.setup();
|
|
126
|
+
mocks.entries = [createPanelAppEntry()];
|
|
127
|
+
|
|
128
|
+
render(<PanelAppsList onOpenPanelApp={vi.fn()} />);
|
|
129
|
+
|
|
130
|
+
expect(screen.getByText('Demo Panel')).toBeTruthy();
|
|
131
|
+
|
|
132
|
+
await user.click(screen.getByRole('button', { name: 'Favorites' }));
|
|
133
|
+
|
|
134
|
+
expect(screen.getByText('No panel apps in this view')).toBeTruthy();
|
|
135
|
+
expect(screen.queryByText('Create your first panel app')).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMemo, useState, type ReactNode } from 'react';
|
|
2
|
-
import { HelpCircle, RefreshCw } from 'lucide-react';
|
|
2
|
+
import { AppWindow, FileCode2, HelpCircle, MessageSquarePlus, RefreshCw } from 'lucide-react';
|
|
3
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
4
|
import { useAppPresenter } from '@/app/components/app-presenter-provider';
|
|
4
5
|
import { PanelAppListItem } from '@/features/panel-apps/components/panel-app-list-item';
|
|
5
6
|
import { useDeletePanelApp, useGrantPanelAppClient, usePanelApps, useRecordPanelAppOpened, useUpdatePanelAppPreferences } from '@/features/panel-apps/hooks/use-panel-apps';
|
|
@@ -10,6 +11,8 @@ import { Tabs, TabsList, TabsTrigger } from '@/shared/components/ui/tabs';
|
|
|
10
11
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/shared/components/ui/tooltip';
|
|
11
12
|
import { t } from '@/shared/lib/i18n';
|
|
12
13
|
|
|
14
|
+
const EMPTY_PANEL_APP_ENTRIES: PanelAppEntryView[] = [];
|
|
15
|
+
|
|
13
16
|
export function PanelAppsList({
|
|
14
17
|
headerContent,
|
|
15
18
|
onOpenPanelApp,
|
|
@@ -23,10 +26,12 @@ export function PanelAppsList({
|
|
|
23
26
|
const recordOpened = useRecordPanelAppOpened();
|
|
24
27
|
const grantClient = useGrantPanelAppClient();
|
|
25
28
|
const presenter = useAppPresenter();
|
|
29
|
+
const navigate = useNavigate();
|
|
26
30
|
const [viewMode, setViewMode] = useState<PanelAppViewMode>('smart');
|
|
31
|
+
const allEntries = panelApps.data?.entries ?? EMPTY_PANEL_APP_ENTRIES;
|
|
27
32
|
const entries = useMemo(
|
|
28
|
-
() => getPanelAppViewEntries(
|
|
29
|
-
[
|
|
33
|
+
() => getPanelAppViewEntries(allEntries, viewMode),
|
|
34
|
+
[allEntries, viewMode],
|
|
30
35
|
);
|
|
31
36
|
|
|
32
37
|
const openPanelApp = async (entry: PanelAppEntryView) => {
|
|
@@ -71,6 +76,11 @@ export function PanelAppsList({
|
|
|
71
76
|
deletePanelApp.mutate(entry.id);
|
|
72
77
|
};
|
|
73
78
|
|
|
79
|
+
const startExamplePanelAppDraft = () => {
|
|
80
|
+
navigate('/chat');
|
|
81
|
+
presenter.chatDraftIntentManager.requestDraft(t('panelAppsExamplePrompt'));
|
|
82
|
+
};
|
|
83
|
+
|
|
74
84
|
if (panelApps.isLoading) {
|
|
75
85
|
return (
|
|
76
86
|
<div className="flex h-full items-center justify-center text-sm text-gray-500">{t('panelAppsLoading')}</div>
|
|
@@ -122,8 +132,14 @@ export function PanelAppsList({
|
|
|
122
132
|
</TabsList>
|
|
123
133
|
</Tabs>
|
|
124
134
|
</div>
|
|
125
|
-
{
|
|
126
|
-
<
|
|
135
|
+
{allEntries.length === 0 ? (
|
|
136
|
+
<PanelAppsEmptyGuide
|
|
137
|
+
panelsPath={panelApps.data?.panelsPath}
|
|
138
|
+
onCreateExample={startExamplePanelAppDraft}
|
|
139
|
+
onRefresh={() => void panelApps.refetch()}
|
|
140
|
+
/>
|
|
141
|
+
) : entries.length === 0 ? (
|
|
142
|
+
<div className="flex flex-1 items-center justify-center px-6 text-center text-sm text-gray-500">{t('panelAppsEmptyFiltered')}</div>
|
|
127
143
|
) : (
|
|
128
144
|
<div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto p-3">
|
|
129
145
|
<div className="space-y-1.5">
|
|
@@ -144,3 +160,101 @@ export function PanelAppsList({
|
|
|
144
160
|
</div>
|
|
145
161
|
);
|
|
146
162
|
}
|
|
163
|
+
|
|
164
|
+
function PanelAppsEmptyGuide({
|
|
165
|
+
onCreateExample,
|
|
166
|
+
panelsPath,
|
|
167
|
+
onRefresh,
|
|
168
|
+
}: {
|
|
169
|
+
onCreateExample: () => void;
|
|
170
|
+
panelsPath?: string;
|
|
171
|
+
onRefresh: () => void;
|
|
172
|
+
}) {
|
|
173
|
+
return (
|
|
174
|
+
<div className="flex flex-1 items-center justify-center overflow-y-auto px-5 py-6 text-center">
|
|
175
|
+
<div className="w-full max-w-sm">
|
|
176
|
+
<div className="mx-auto flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
177
|
+
<AppWindow className="h-5 w-5" />
|
|
178
|
+
</div>
|
|
179
|
+
<h2 className="mt-3 text-sm font-semibold text-gray-900">{t('panelAppsEmptyTitle')}</h2>
|
|
180
|
+
<p className="mt-1 text-xs leading-5 text-gray-500">{t('panelAppsEmptyDescription')}</p>
|
|
181
|
+
|
|
182
|
+
<div className="mt-4 space-y-2 text-left">
|
|
183
|
+
<PanelAppsEmptyGuideStep
|
|
184
|
+
icon={<MessageSquarePlus className="h-4 w-4" />}
|
|
185
|
+
title={t('panelAppsEmptyAskTitle')}
|
|
186
|
+
description={t('panelAppsEmptyAskDescription')}
|
|
187
|
+
actionLabel={t('panelAppsEmptyAskAction')}
|
|
188
|
+
onAction={onCreateExample}
|
|
189
|
+
/>
|
|
190
|
+
<PanelAppsEmptyGuideStep
|
|
191
|
+
icon={<FileCode2 className="h-4 w-4" />}
|
|
192
|
+
title={t('panelAppsEmptyFileTitle')}
|
|
193
|
+
description={t('panelAppsEmptyFileDescription')}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{panelsPath ? (
|
|
198
|
+
<div className="mt-4 rounded-md bg-gray-50 px-3 py-2 text-left">
|
|
199
|
+
<div className="text-[11px] font-medium uppercase text-gray-400">{t('panelAppsPanelsPath')}</div>
|
|
200
|
+
<code className="mt-1 block break-all text-xs text-gray-600">{panelsPath}</code>
|
|
201
|
+
</div>
|
|
202
|
+
) : null}
|
|
203
|
+
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
onClick={onRefresh}
|
|
207
|
+
className="mt-4 inline-flex items-center justify-center gap-1.5 rounded-md border border-gray-200 px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
|
|
208
|
+
>
|
|
209
|
+
<RefreshCw className="h-3.5 w-3.5" />
|
|
210
|
+
{t('panelAppsRefresh')}
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function PanelAppsEmptyGuideStep({
|
|
218
|
+
actionLabel,
|
|
219
|
+
description,
|
|
220
|
+
icon,
|
|
221
|
+
onAction,
|
|
222
|
+
title,
|
|
223
|
+
}: {
|
|
224
|
+
actionLabel?: string;
|
|
225
|
+
description: string;
|
|
226
|
+
icon: ReactNode;
|
|
227
|
+
onAction?: () => void;
|
|
228
|
+
title: string;
|
|
229
|
+
}) {
|
|
230
|
+
const content = (
|
|
231
|
+
<>
|
|
232
|
+
<div className="mt-0.5 text-gray-500">{icon}</div>
|
|
233
|
+
<div className="min-w-0">
|
|
234
|
+
<div className="text-xs font-medium text-gray-800">{title}</div>
|
|
235
|
+
<div className="mt-0.5 text-xs leading-5 text-gray-500">{description}</div>
|
|
236
|
+
{actionLabel ? (
|
|
237
|
+
<div className="mt-2 text-xs font-medium text-primary">{actionLabel}</div>
|
|
238
|
+
) : null}
|
|
239
|
+
</div>
|
|
240
|
+
</>
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (onAction) {
|
|
244
|
+
return (
|
|
245
|
+
<button
|
|
246
|
+
type="button"
|
|
247
|
+
onClick={onAction}
|
|
248
|
+
className="flex w-full gap-2 rounded-md bg-gray-50 px-3 py-2.5 text-left transition-colors hover:bg-primary/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
|
|
249
|
+
>
|
|
250
|
+
{content}
|
|
251
|
+
</button>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<div className="flex gap-2 rounded-md bg-gray-50 px-3 py-2.5">
|
|
257
|
+
{content}
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { PanelAppsList } from './components/panel-apps-list';
|
|
2
|
+
export { usePanelApps } from './hooks/use-panel-apps';
|
|
2
3
|
export { PanelAppBridgeManager } from './managers/panel-app-bridge.manager';
|
|
4
|
+
export { findPanelAppEntryByDisplayId } from './utils/panel-app-entry-match.utils';
|
|
3
5
|
export {
|
|
4
6
|
openApps,
|
|
5
7
|
PANEL_APPS_DOC_BROWSER_RENDERERS,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
2
|
+
|
|
3
|
+
function isPanelAppEntryMatch(entry: PanelAppEntryView, value: string): boolean {
|
|
4
|
+
const normalizedValue = value.trim();
|
|
5
|
+
return [entry.id, entry.appId, entry.fileName, entry.title].some((candidate) => candidate.trim() === normalizedValue);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function findPanelAppEntryByDisplayId(
|
|
9
|
+
entries: readonly PanelAppEntryView[],
|
|
10
|
+
value: string,
|
|
11
|
+
): PanelAppEntryView | null {
|
|
12
|
+
const normalizedValue = value.trim();
|
|
13
|
+
if (!normalizedValue) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return entries.find((entry) => isPanelAppEntryMatch(entry, normalizedValue)) ?? null;
|
|
17
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { RuntimeModelSelectionMode } from '@nextclaw/shared';
|
|
2
|
+
import type { ThinkingLevel } from './types';
|
|
3
|
+
|
|
4
|
+
export type RuntimeDefaultThinkingView = {
|
|
5
|
+
supported: ThinkingLevel[];
|
|
6
|
+
default?: ThinkingLevel | null;
|
|
7
|
+
};
|
|
2
8
|
|
|
3
9
|
export type ChatSessionTypeIconView = {
|
|
4
10
|
kind: "image";
|
|
@@ -22,6 +28,7 @@ export type ChatSessionTypeOptionView = {
|
|
|
22
28
|
supportedModels?: string[];
|
|
23
29
|
recommendedModel?: string | null;
|
|
24
30
|
modelSelectionMode?: RuntimeModelSelectionMode;
|
|
31
|
+
runtimeDefaultThinking?: RuntimeDefaultThinkingView | null;
|
|
25
32
|
cta?: ChatSessionTypeCtaView | null;
|
|
26
33
|
};
|
|
27
34
|
|
|
@@ -256,6 +256,7 @@ export type AgentProfileView = {
|
|
|
256
256
|
runtimeConfig?: Record<string, unknown>;
|
|
257
257
|
engine?: string;
|
|
258
258
|
engineConfig?: Record<string, unknown>;
|
|
259
|
+
thinkingDefault?: ThinkingLevel;
|
|
259
260
|
contextTokens?: number;
|
|
260
261
|
maxToolIterations?: number;
|
|
261
262
|
builtIn?: boolean;
|
|
@@ -447,6 +448,7 @@ export type RuntimeConfigUpdate = {
|
|
|
447
448
|
contextTokens?: number;
|
|
448
449
|
engine?: string;
|
|
449
450
|
engineConfig?: Record<string, unknown>;
|
|
451
|
+
thinkingDefault?: ThinkingLevel;
|
|
450
452
|
};
|
|
451
453
|
runtimes?: {
|
|
452
454
|
entries?: Record<string, RuntimeEntryView> | null;
|
|
@@ -520,6 +522,7 @@ export type ConfigView = {
|
|
|
520
522
|
workspace?: string;
|
|
521
523
|
engine?: string;
|
|
522
524
|
engineConfig?: Record<string, unknown>;
|
|
525
|
+
thinkingDefault?: ThinkingLevel;
|
|
523
526
|
contextTokens?: number;
|
|
524
527
|
maxToolIterations?: number;
|
|
525
528
|
};
|
|
@@ -101,6 +101,9 @@
|
|
|
101
101
|
"chatDeleteSession": "Delete Session",
|
|
102
102
|
"chatDeleteSessionConfirm": "Delete the current session?",
|
|
103
103
|
"chatSessionMoreActions": "More actions",
|
|
104
|
+
"chatSessionViewMetadata": "View Metadata",
|
|
105
|
+
"chatSessionMetadataDialogTitle": "Session Metadata",
|
|
106
|
+
"chatSessionMetadataDialogDescription": "Raw metadata stored on the current session.",
|
|
104
107
|
"chatSessionSetProject": "Set Project Directory",
|
|
105
108
|
"chatSessionClearProject": "Clear Project Directory",
|
|
106
109
|
"chatSessionProjectDialogTitle": "Set Project Directory",
|
|
@@ -129,6 +132,7 @@
|
|
|
129
132
|
"chatToolStatusFailed": "Failed",
|
|
130
133
|
"chatToolStatusCancelled": "Cancelled",
|
|
131
134
|
"chatShowContentAction": "Show content",
|
|
135
|
+
"chatPanelCardExpand": "Expand to side panel",
|
|
132
136
|
"chatReasoning": "Reasoning",
|
|
133
137
|
"chatImageAttachment": "Image attachment",
|
|
134
138
|
"chatFileAttachment": "File attachment",
|
|
@@ -22,6 +22,16 @@
|
|
|
22
22
|
"panelAppsLoading": "Loading panel apps...",
|
|
23
23
|
"panelAppsLoadFailed": "Failed to load panel apps",
|
|
24
24
|
"panelAppsEmpty": "No panel apps yet",
|
|
25
|
+
"panelAppsEmptyTitle": "Create your first panel app",
|
|
26
|
+
"panelAppsEmptyDescription": "Panel apps are lightweight tools that open beside chat, such as dashboards, forms, calculators, and temporary workbenches.",
|
|
27
|
+
"panelAppsEmptyAskTitle": "Ask NextClaw to generate one",
|
|
28
|
+
"panelAppsEmptyAskDescription": "Start with a ready-made prompt that creates a sample local tool for this panel.",
|
|
29
|
+
"panelAppsEmptyAskAction": "Create sample panel app",
|
|
30
|
+
"panelAppsExamplePrompt": "Create a sample NextClaw Panel App for me: a personal task board. Put it in the NextClaw panels folder as a single *.panel.html file. Include adding tasks, toggling completion, filtering, and local state persistence. After creating it, tell me to refresh Panel Apps and open it. Do not ask follow-up questions first.",
|
|
31
|
+
"panelAppsEmptyFileTitle": "Add it to the panels folder",
|
|
32
|
+
"panelAppsEmptyFileDescription": "You can also add a *.panel.html file, or a *.panel folder with panel-app.json.",
|
|
33
|
+
"panelAppsPanelsPath": "Panels folder",
|
|
34
|
+
"panelAppsEmptyFiltered": "No panel apps in this view",
|
|
25
35
|
"panelAppsRefresh": "Refresh panel apps",
|
|
26
36
|
"panelAppsRefreshCurrent": "Refresh current panel app",
|
|
27
37
|
"panelAppsSortLabel": "Sort",
|
|
@@ -101,6 +101,9 @@
|
|
|
101
101
|
"chatDeleteSession": "删除会话",
|
|
102
102
|
"chatDeleteSessionConfirm": "确认删除当前会话?",
|
|
103
103
|
"chatSessionMoreActions": "更多操作",
|
|
104
|
+
"chatSessionViewMetadata": "查看元数据",
|
|
105
|
+
"chatSessionMetadataDialogTitle": "会话元数据",
|
|
106
|
+
"chatSessionMetadataDialogDescription": "当前会话保存的原始元数据。",
|
|
104
107
|
"chatSessionSetProject": "设置项目目录",
|
|
105
108
|
"chatSessionClearProject": "清除项目目录",
|
|
106
109
|
"chatSessionProjectDialogTitle": "设置项目目录",
|
|
@@ -129,6 +132,7 @@
|
|
|
129
132
|
"chatToolStatusFailed": "失败",
|
|
130
133
|
"chatToolStatusCancelled": "已取消",
|
|
131
134
|
"chatShowContentAction": "展示内容",
|
|
135
|
+
"chatPanelCardExpand": "展开到侧栏",
|
|
132
136
|
"chatReasoning": "推理过程",
|
|
133
137
|
"chatImageAttachment": "图片附件",
|
|
134
138
|
"chatFileAttachment": "文件附件",
|
|
@@ -22,6 +22,16 @@
|
|
|
22
22
|
"panelAppsLoading": "加载面板应用中...",
|
|
23
23
|
"panelAppsLoadFailed": "面板应用加载失败",
|
|
24
24
|
"panelAppsEmpty": "暂无面板应用",
|
|
25
|
+
"panelAppsEmptyTitle": "创建你的第一个面板应用",
|
|
26
|
+
"panelAppsEmptyDescription": "面板应用是在聊天旁边打开的轻量工具,适合仪表盘、表单、计算器和临时工作台。",
|
|
27
|
+
"panelAppsEmptyAskTitle": "让 NextClaw 帮你生成",
|
|
28
|
+
"panelAppsEmptyAskDescription": "从一段现成提示词开始,让 NextClaw 直接创建一个可打开的示例小工具。",
|
|
29
|
+
"panelAppsEmptyAskAction": "生成示例面板应用",
|
|
30
|
+
"panelAppsExamplePrompt": "请直接为我创建一个示例 NextClaw Panel App:个人任务看板。请把它放到 NextClaw panels 目录,使用单文件 *.panel.html,包含任务添加、完成状态切换、筛选和本地状态保存。创建完成后告诉我刷新 Panel Apps 并打开它。不要先问我问题。",
|
|
31
|
+
"panelAppsEmptyFileTitle": "放入 panels 目录",
|
|
32
|
+
"panelAppsEmptyFileDescription": "也可以添加 *.panel.html 文件,或带 panel-app.json 的 *.panel 目录。",
|
|
33
|
+
"panelAppsPanelsPath": "面板目录",
|
|
34
|
+
"panelAppsEmptyFiltered": "当前分类暂无面板应用",
|
|
25
35
|
"panelAppsRefresh": "刷新面板应用",
|
|
26
36
|
"panelAppsRefreshCurrent": "刷新当前面板应用",
|
|
27
37
|
"panelAppsSortLabel": "排序",
|