@nextclaw/ui 0.14.0 → 0.14.1-beta.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 +41 -0
- package/dist/assets/{api-B50yVdT8.js → api-dQ6uQSZG.js} +3 -3
- package/dist/assets/channels-list-page-DfpjB8XB.js +8 -0
- package/dist/assets/chat-page-D3QaEqjs.js +105 -0
- package/dist/assets/config-split-page-CeFfbaqc.js +1 -0
- package/dist/assets/{confirm-dialog-DRSelLdI.js → confirm-dialog-C95Y4QPC.js} +1 -1
- package/dist/assets/desktop-update-config-CcK6jiEb.js +1 -0
- package/dist/assets/{dist-D19wiQwB.js → dist-DTGzeil6.js} +1 -1
- package/dist/assets/{dist-NZRV-BxD.js → dist-fGogBmPA.js} +1 -1
- package/dist/assets/doc-browser-CuBTcBEZ.js +1 -0
- package/dist/assets/doc-browser-DVHhEb96.js +1 -0
- package/dist/assets/{doc-browser-context-C8KPbj-p.js → doc-browser-context-BfMXwnrz.js} +1 -1
- package/dist/assets/{ellipsis-BYy8TP0A.js → ellipsis-D2F4KMiU.js} +1 -1
- package/dist/assets/{external-link-528k6MDD.js → external-link-CcgtBK26.js} +1 -1
- package/dist/assets/index-ByJCbQqr.css +1 -0
- package/dist/assets/index-DjXqiloY.js +103 -0
- package/dist/assets/mcp-marketplace-page-98-V__3u.js +1 -0
- package/dist/assets/mcp-marketplace-page-BNq_PNmO.js +40 -0
- package/dist/assets/model-config-page-aAUkZkSX.js +1 -0
- package/dist/assets/plus-1Rxl5FlV.js +1 -0
- package/dist/assets/provider-scoped-model-input-we4_Z4gP.js +1 -0
- package/dist/assets/providers-config-page-CtMkPSyu.js +1 -0
- package/dist/assets/{react-Dsotw5vM.js → react-CTqdhFMk.js} +1 -1
- package/dist/assets/remote-fQ7N_r8u.js +1 -0
- package/dist/assets/rotate-cw-CzfIeAJj.js +1 -0
- package/dist/assets/runtime-config-page-TD1UrTbG.js +1 -0
- package/dist/assets/{save-CHDnDTok.js → save-C9fKWfVB.js} +1 -1
- package/dist/assets/search-D5FQ5q0X.js +1 -0
- package/dist/assets/search-config-page-AMEJJNIS.js +1 -0
- package/dist/assets/secrets-config-page-CcgJnvBO.js +3 -0
- package/dist/assets/select-Cu5n0E-3.js +41 -0
- package/dist/assets/{tag-chip-Cr8GwpRE.js → tag-chip-C5aOtjxu.js} +1 -1
- package/dist/assets/use-config-D8TESeo6.js +1 -0
- package/dist/assets/x-1qETdQ70.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/__tests__/chat-conversation-panel.test.tsx +12 -63
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +11 -50
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +1 -0
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +15 -2
- package/src/features/chat/features/input/components/chat-input-bar.container.tsx +122 -85
- package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +120 -0
- package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +87 -0
- package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +66 -41
- package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +1 -0
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +22 -36
- package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +80 -52
- 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-file-operation.utils.test.ts +310 -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/__tests__/chat-message-test.utils.ts +43 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +1 -346
- 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 +14 -9
- package/src/features/chat/features/message/utils/file-operation/diff.utils.ts +39 -36
- package/src/features/chat/features/message/utils/file-operation/line-builder.utils.ts +46 -38
- package/src/features/chat/features/message/utils/file-operation/parsed-block.utils.ts +18 -15
- 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/__tests__/chat-session-project-badge.test.tsx +25 -25
- 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/components/session-header/chat-session-project-badge.tsx +15 -2
- 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/components/__tests__/chat-sidebar-create-menu.test.tsx +24 -0
- package/src/features/chat/features/session-type/components/chat-sidebar-create-menu.tsx +10 -1
- 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/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +193 -0
- package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +123 -0
- package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +104 -0
- package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +72 -0
- package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +54 -0
- package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +132 -0
- package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +82 -0
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +121 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +53 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-selection.utils.test.ts +63 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-visibility.utils.test.ts +34 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +71 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-selection.utils.ts +79 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-visibility.utils.ts +17 -0
- package/src/features/chat/index.ts +1 -0
- package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +93 -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 +37 -15
- 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 +9 -1
- package/src/features/chat/stores/recent-selection.store.ts +85 -0
- package/src/features/chat/types/chat-input-bar.types.ts +5 -1
- package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +27 -23
- 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/features/settings/components/config/provider-form.tsx +284 -387
- package/src/features/settings/components/config/provider-status-badge.tsx +4 -4
- package/src/features/settings/components/config/secrets-config-form.tsx +11 -135
- package/src/features/settings/hooks/use-provider-auth-flow.ts +140 -0
- package/src/features/settings/pages/__tests__/model-config-page.test.tsx +16 -0
- package/src/features/settings/pages/secrets-config-page.tsx +2 -1
- package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +60 -1
- package/src/features/settings/utils/__tests__/secrets-config-form.utils.test.ts +66 -0
- package/src/features/settings/utils/provider-form-context.utils.ts +131 -0
- package/src/features/settings/utils/provider-form-model.utils.ts +101 -0
- package/src/features/settings/utils/provider-form-support.utils.ts +102 -1
- package/src/features/settings/utils/secrets-config-form.utils.ts +139 -0
- package/src/features/system-status/components/config/runtime-agent-list-card.tsx +17 -11
- package/src/features/system-status/components/config/runtime-binding-list-card.tsx +14 -9
- package/src/features/system-status/components/config/runtime-config-editor.tsx +8 -5
- package/src/features/system-status/components/config/runtime-entry-list-card.tsx +14 -9
- package/src/features/system-status/components/config/runtime-settings-card.tsx +16 -7
- package/src/features/system-status/components/runtime-security-card.tsx +79 -25
- package/src/shared/components/common/__tests__/searchable-model-input.test.tsx +34 -0
- package/src/shared/components/common/provider-scoped-model-input.tsx +1 -0
- package/src/shared/components/common/searchable-model-input.tsx +102 -87
- package/src/shared/components/ui/README.md +7 -0
- package/src/shared/components/ui/popover.tsx +19 -3
- package/src/shared/components/ui/select.tsx +14 -4
- package/src/shared/hooks/use-infinite-scroll-loader.ts +14 -10
- package/src/shared/lib/api/chat-session-type.types.ts +7 -0
- package/src/shared/lib/api/index.ts +2 -0
- package/src/shared/lib/api/ncp-session.types.ts +1 -0
- package/src/shared/lib/api/preferences/preference.types.ts +22 -0
- package/src/shared/lib/api/preferences/preference.utils.ts +40 -0
- package/src/shared/lib/api/types.ts +3 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +20 -1
- package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +10 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -1
- package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -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/secrets-config-page-95yaH1fa.js +0 -3
- 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/use-config-TzwzTgg0.js +0 -1
- package/dist/assets/x-fkIWvi07.js +0 -1
- package/src/features/chat/components/__tests__/chat-welcome.test.tsx +0 -45
- package/src/features/chat/components/chat-welcome.tsx +0 -102
- 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
|
@@ -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,11 @@ export type ChatInputBarAdapterTexts = {
|
|
|
37
37
|
modelNoOptionsLabel: string;
|
|
38
38
|
recentModelsLabel: string;
|
|
39
39
|
allModelsLabel: string;
|
|
40
|
-
|
|
40
|
+
favoriteModelsLabel: string;
|
|
41
|
+
modelSearchPlaceholder: string;
|
|
42
|
+
modelSearchEmptyLabel: string;
|
|
43
|
+
favoriteModelLabel: string;
|
|
44
|
+
unfavoriteModelLabel: string;
|
|
41
45
|
thinkingLabels: Record<ChatThinkingLevel, string>;
|
|
42
46
|
noModelOptionsLabel: string;
|
|
43
47
|
configureProviderLabel: string;
|
|
@@ -16,29 +16,30 @@ function buildInstalledRecordFromInstall(params: {
|
|
|
16
16
|
request: MarketplaceInstallRequest;
|
|
17
17
|
result: MarketplaceInstallResult;
|
|
18
18
|
}): MarketplaceInstalledRecord {
|
|
19
|
+
const { request, result } = params;
|
|
19
20
|
const installedAt = new Date().toISOString();
|
|
20
21
|
|
|
21
|
-
if (
|
|
22
|
+
if (result.type === 'skill') {
|
|
22
23
|
return {
|
|
23
24
|
type: 'skill',
|
|
24
|
-
spec:
|
|
25
|
-
id:
|
|
26
|
-
label:
|
|
25
|
+
spec: result.spec,
|
|
26
|
+
id: request.skill ?? result.spec,
|
|
27
|
+
label: request.skill ?? result.name ?? result.spec,
|
|
27
28
|
source: 'workspace',
|
|
28
|
-
installPath:
|
|
29
|
+
installPath: request.installPath,
|
|
29
30
|
installedAt
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
return {
|
|
34
|
-
type:
|
|
35
|
-
spec:
|
|
36
|
-
id:
|
|
37
|
-
label:
|
|
35
|
+
type: result.type,
|
|
36
|
+
spec: result.spec,
|
|
37
|
+
id: result.name ?? result.spec,
|
|
38
|
+
label: result.name ?? result.spec,
|
|
38
39
|
source: 'marketplace',
|
|
39
40
|
origin: 'marketplace',
|
|
40
|
-
enabled:
|
|
41
|
-
runtimeStatus:
|
|
41
|
+
enabled: request.enabled ?? true,
|
|
42
|
+
runtimeStatus: request.enabled === false ? 'disabled' : 'ready',
|
|
42
43
|
installedAt
|
|
43
44
|
};
|
|
44
45
|
}
|
|
@@ -47,10 +48,11 @@ function matchesInstalledRecord(record: MarketplaceInstalledRecord, params: {
|
|
|
47
48
|
id?: string;
|
|
48
49
|
spec?: string;
|
|
49
50
|
}): boolean {
|
|
50
|
-
|
|
51
|
+
const { id, spec } = params;
|
|
52
|
+
if (spec && record.spec === spec) {
|
|
51
53
|
return true;
|
|
52
54
|
}
|
|
53
|
-
if (
|
|
55
|
+
if (id && record.id === id) {
|
|
54
56
|
return true;
|
|
55
57
|
}
|
|
56
58
|
return false;
|
|
@@ -70,7 +72,8 @@ export function applyInstallResultToInstalledView(params: {
|
|
|
70
72
|
request: MarketplaceInstallRequest;
|
|
71
73
|
result: MarketplaceInstallResult;
|
|
72
74
|
}): MarketplaceInstalledView {
|
|
73
|
-
const
|
|
75
|
+
const { result, view } = params;
|
|
76
|
+
const current = ensureInstalledView(result.type, view);
|
|
74
77
|
const optimisticRecord = buildInstalledRecordFromInstall(params);
|
|
75
78
|
const existingIndex = current.records.findIndex((record) => matchesInstalledRecord(record, {
|
|
76
79
|
id: optimisticRecord.id,
|
|
@@ -89,7 +92,7 @@ export function applyInstallResultToInstalledView(params: {
|
|
|
89
92
|
|
|
90
93
|
return {
|
|
91
94
|
...current,
|
|
92
|
-
type:
|
|
95
|
+
type: result.type,
|
|
93
96
|
records: nextRecords,
|
|
94
97
|
specs: dedupeSpecs(nextRecords),
|
|
95
98
|
total: nextRecords.length
|
|
@@ -101,12 +104,13 @@ export function applyManageResultToInstalledView(params: {
|
|
|
101
104
|
request: MarketplaceManageRequest;
|
|
102
105
|
result: MarketplaceManageResult;
|
|
103
106
|
}): MarketplaceInstalledView {
|
|
104
|
-
const
|
|
107
|
+
const { request, result, view } = params;
|
|
108
|
+
const current = ensureInstalledView(result.type, view);
|
|
105
109
|
|
|
106
|
-
if (
|
|
110
|
+
if (result.action === 'uninstall' || result.action === 'remove') {
|
|
107
111
|
const nextRecords = current.records.filter((record) => !matchesInstalledRecord(record, {
|
|
108
|
-
id:
|
|
109
|
-
spec:
|
|
112
|
+
id: result.id,
|
|
113
|
+
spec: request.spec
|
|
110
114
|
}));
|
|
111
115
|
|
|
112
116
|
return {
|
|
@@ -119,13 +123,13 @@ export function applyManageResultToInstalledView(params: {
|
|
|
119
123
|
|
|
120
124
|
const nextRecords = current.records.map((record) => {
|
|
121
125
|
if (!matchesInstalledRecord(record, {
|
|
122
|
-
id:
|
|
123
|
-
spec:
|
|
126
|
+
id: result.id,
|
|
127
|
+
spec: request.spec
|
|
124
128
|
})) {
|
|
125
129
|
return record;
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
if (
|
|
132
|
+
if (result.action === 'disable') {
|
|
129
133
|
return {
|
|
130
134
|
...record,
|
|
131
135
|
enabled: false,
|
|
@@ -133,7 +137,7 @@ export function applyManageResultToInstalledView(params: {
|
|
|
133
137
|
};
|
|
134
138
|
}
|
|
135
139
|
|
|
136
|
-
if (
|
|
140
|
+
if (result.action === 'update') {
|
|
137
141
|
return {
|
|
138
142
|
...record,
|
|
139
143
|
installedAt: new Date().toISOString(),
|
|
@@ -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
|
+
}
|