@nextclaw/ui 0.12.8 → 0.12.9
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 +35 -0
- package/dist/assets/ChannelsList-Ita2Zm1_.js +8 -0
- package/dist/assets/{DocBrowser-BMxf9CIK.js → DocBrowser-6ReNjvzF.js} +1 -1
- package/dist/assets/DocBrowser-BNwbPHf4.js +1 -0
- package/dist/assets/{DocBrowserContext-Ce28gRXt.js → DocBrowserContext-B6SpA7Qs.js} +1 -1
- package/dist/assets/{LogoBadge-o92MOA2L.js → LogoBadge-ByNLYg65.js} +1 -1
- package/dist/assets/MarketplacePage-CjX2MWww.js +1 -0
- package/dist/assets/{MarketplacePage-BySqkYDh.js → MarketplacePage-D0sDlYX4.js} +1 -1
- package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +40 -0
- package/dist/assets/{ModelConfig-IrmzoslW.js → ModelConfig-BzZenCH-.js} +1 -1
- package/dist/assets/{ProviderScopedModelInput-CmTIzgI7.js → ProviderScopedModelInput-Da7khnBA.js} +1 -1
- package/dist/assets/{ProvidersList-8_Kalfwl.js → ProvidersList-BbVzRxjY.js} +1 -1
- package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +1 -0
- package/dist/assets/RuntimeConfig-F_XKGgLm.js +1 -0
- package/dist/assets/{SearchConfig-DNBR-UbE.js → SearchConfig-BGkzXQP-.js} +1 -1
- package/dist/assets/{SecretsConfig-Ba1RPJaG.js → SecretsConfig-D281Rotl.js} +2 -2
- package/dist/assets/{SessionsConfig-Doqp5ghH.js → SessionsConfig-ChHQ7M5c.js} +2 -2
- package/dist/assets/{app-query-client-DniXoIN5.js → app-query-client-VnFElj4E.js} +1 -1
- package/dist/assets/{book-open-DocgeQtR.js → book-open-BdcxxoQu.js} +1 -1
- package/dist/assets/chat-page-Doe0yTtB.js +58 -0
- package/dist/assets/chat-session-display-cw78aiI_.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-BvKvh1R8.js → chunk-JZWAC4HX-DK5HPmIK.js} +1 -1
- package/dist/assets/{client-CVqPF5ie.js → client-_i4MU2bB.js} +1 -1
- package/dist/assets/{config-Bop2oB18.js → config-DtIQwrHF.js} +1 -1
- package/dist/assets/{createLucideIcon-DVv8taGY.js → createLucideIcon-BSeTgkZW.js} +1 -1
- package/dist/assets/desktop-update-config-Dpcf4BKG.js +1 -0
- package/dist/assets/{dist-Da5Gm_pO.js → dist-6TrrnPCR.js} +1 -1
- package/dist/assets/{dist-DmAlInRu.js → dist-ccBFUi-o.js} +1 -1
- package/dist/assets/download-BhDxnyvU.js +1 -0
- package/dist/assets/{external-link-DFjw3x1B.js → external-link-BgErLCNT.js} +1 -1
- package/dist/assets/{hash-DJtaCejM.js → hash-Bl7dr_UG.js} +1 -1
- package/dist/assets/i18n-eDHeDY0n.js +1 -0
- package/dist/assets/index-CF9xve0E.js +6 -0
- package/dist/assets/index-FgA52VBt.css +1 -0
- package/dist/assets/{infiniteQueryBehavior-DHSEQ3OH.js → infiniteQueryBehavior-ZDS92Qpp.js} +1 -1
- package/dist/assets/loader-circle-ACM1s51e.js +1 -0
- package/dist/assets/{logos-DEFUIR12.js → logos-x89HbrZ4.js} +1 -1
- package/dist/assets/{page-layout-Da3i3r6G.js → page-layout-vZnghcFy.js} +1 -1
- package/dist/assets/play-CFUwCA2E.js +1 -0
- package/dist/assets/plus-rYsv72JG.js +1 -0
- package/dist/assets/{popover-C_mWOFzI.js → popover-Bg1VoTZ6.js} +1 -1
- package/dist/assets/{refresh-ccw-D6HkNtfz.js → refresh-ccw-DT98i__E.js} +1 -1
- package/dist/assets/{refresh-cw-DRcvRrnc.js → refresh-cw-C47QSEwg.js} +1 -1
- package/dist/assets/{rotate-cw-BmDKfXtH.js → rotate-cw-JtFzpNn6.js} +1 -1
- package/dist/assets/{save-DHGmi2e9.js → save-3S6-H3Xw.js} +1 -1
- package/dist/assets/search-3kFR_zh9.js +1 -0
- package/dist/assets/{security-config-CbXfPZzr.js → security-config-BWaiARNk.js} +1 -1
- package/dist/assets/{select-Caud8QvU.js → select-DJ2MUjBB.js} +1 -1
- package/dist/assets/skeleton-ByQepn0M.js +1 -0
- package/dist/assets/{status-dot-DurKKSwA.js → status-dot-vbanNPFU.js} +1 -1
- package/dist/assets/{switch-0rmPBRKI.js → switch-BsLtHOH-.js} +1 -1
- package/dist/assets/{tabs-custom-5JLVL6v8.js → tabs-custom-D3HYMt6k.js} +1 -1
- package/dist/assets/{trash-2-C6caKPoz.js → trash-2-G48scll7.js} +1 -1
- package/dist/assets/{use-infinite-scroll-loader-dwnaa_qi.js → use-infinite-scroll-loader-DkNhD-42.js} +1 -1
- package/dist/assets/{useConfirmDialog-mMeWD_yo.js → useConfirmDialog-BkvTN-vd.js} +1 -1
- package/dist/assets/{useMutation-BmxxvCNf.js → useMutation-CBWjE2uj.js} +1 -1
- package/dist/assets/x-ByDbItbq.js +1 -0
- package/dist/index.html +95 -21
- package/dist/manifest.webmanifest +30 -0
- package/dist/offline.html +102 -0
- package/dist/pwa-192.png +0 -0
- package/dist/pwa-512.png +0 -0
- package/dist/sw.js +80 -0
- package/index.html +73 -1
- package/package.json +6 -6
- package/public/manifest.webmanifest +30 -0
- package/public/offline.html +102 -0
- package/public/pwa-192.png +0 -0
- package/public/pwa-512.png +0 -0
- package/public/sw.js +80 -0
- package/src/api/server-path.ts +27 -4
- package/src/api/types.ts +17 -10
- package/src/app.tsx +9 -0
- package/src/components/chat/ChatSidebar.test.tsx +43 -1
- package/src/components/chat/ChatSidebar.tsx +24 -0
- package/src/components/chat/adapters/chat-message.summary-truncation.test.ts +66 -0
- package/src/components/chat/adapters/file-operation/card.ts +9 -0
- package/src/components/chat/adapters/file-operation/diff.ts +14 -0
- package/src/components/chat/{ChatConversationPanel.test.tsx → chat-conversation-panel.test.tsx} +107 -206
- package/src/components/chat/chat-conversation-panel.tsx +412 -0
- package/src/components/chat/chat-page-shell.tsx +1 -1
- package/src/components/chat/chat-session-workspace-file-preview.test.tsx +91 -0
- package/src/components/chat/chat-session-workspace-file-preview.tsx +307 -0
- package/src/components/chat/chat-session-workspace-panel-nav.tsx +197 -0
- package/src/components/chat/chat-session-workspace-panel.tsx +318 -0
- package/src/components/chat/chat-sidebar-session-item.tsx +32 -2
- package/src/components/chat/containers/chat-message-list.container.test.tsx +49 -0
- package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
- package/src/components/chat/managers/chat-session-list.manager.test.ts +12 -0
- package/src/components/chat/managers/chat-session-list.manager.ts +7 -0
- package/src/components/chat/ncp/ncp-chat-page.tsx +7 -7
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +179 -41
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +35 -1
- package/src/components/chat/ncp/ncp-session-adapter.ts +17 -0
- package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +54 -11
- package/src/components/chat/ncp/tests/ncp-chat-thread.manager.test.ts +189 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +13 -2
- package/src/components/chat/session-header/chat-session-header-actions.test.tsx +26 -0
- package/src/components/chat/session-header/chat-session-header-actions.tsx +19 -1
- package/src/components/chat/stores/chat-thread.store.ts +24 -0
- package/src/components/config/RuntimeConfig.tsx +141 -2
- package/src/components/layout/AppLayout.tsx +1 -1
- package/src/components/providers/ThemeProvider.tsx +5 -0
- package/src/hooks/server-path/use-server-path-read.ts +20 -0
- package/src/lib/chat-message.ts +14 -3
- package/src/lib/i18n.chat.ts +12 -1
- package/src/lib/i18n.pwa.ts +62 -0
- package/src/lib/i18n.ts +2 -2
- package/src/pwa/components/pwa-install-entry.test.tsx +110 -0
- package/src/pwa/components/pwa-install-entry.tsx +205 -0
- package/src/pwa/managers/pwa-install.manager.test.ts +160 -0
- package/src/pwa/managers/pwa-install.manager.ts +232 -0
- package/src/pwa/managers/pwa-runtime.manager.ts +196 -0
- package/src/pwa/managers/pwa-shell-theme.manager.test.ts +30 -0
- package/src/pwa/managers/pwa-shell-theme.manager.ts +46 -0
- package/src/pwa/pwa-install-banner.storage.ts +55 -0
- package/src/pwa/pwa.types.ts +22 -0
- package/src/pwa/register-pwa.ts +14 -0
- package/src/pwa/stores/pwa.store.ts +17 -0
- package/src/vite-env.d.ts +9 -0
- package/dist/assets/ChannelsList-KIQIxluX.js +0 -8
- package/dist/assets/DocBrowser-CyDgAtO9.js +0 -1
- package/dist/assets/MarketplacePage-C0olZaek.js +0 -1
- package/dist/assets/McpMarketplacePage-DqKaiXO9.js +0 -40
- package/dist/assets/RemoteAccessPage-CyQlSjPf.js +0 -1
- package/dist/assets/RuntimeConfig-Bk0uYBhf.js +0 -1
- package/dist/assets/chat-page-Bph8M5zo.js +0 -58
- package/dist/assets/chat-session-display-CoN3Wmn-.js +0 -1
- package/dist/assets/desktop-update-config-1KBrqLBC.js +0 -1
- package/dist/assets/i18n-CwHZ-9vt.js +0 -1
- package/dist/assets/index-DafCdM4F.css +0 -1
- package/dist/assets/index-DdksE6U3.js +0 -6
- package/dist/assets/loader-circle-PsSP0H9n.js +0 -1
- package/dist/assets/play-DBQbBxTA.js +0 -1
- package/dist/assets/plus-DUOVbsyQ.js +0 -1
- package/dist/assets/search-MChQRYR1.js +0 -1
- package/dist/assets/skeleton-B-4vRq_Z.js +0 -1
- package/dist/assets/x-DuMhMATD.js +0 -1
- package/src/components/chat/ChatConversationPanel.tsx +0 -256
- package/src/components/chat/chat-child-session-panel.tsx +0 -270
- /package/dist/assets/{config-hints-BZoDjXye.js → config-hints-BhTmc9P1.js} +0 -0
- /package/dist/assets/{config-layout-DmlGaay2.js → config-layout-CHs0mAaR.js} +0 -0
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ChatComposerNode,
|
|
3
|
+
ChatFileOpenActionViewModel,
|
|
4
|
+
ChatToolActionViewModel,
|
|
5
|
+
} from '@nextclaw/agent-chat-ui';
|
|
2
6
|
import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
|
|
3
7
|
import { createContext, useContext } from 'react';
|
|
4
8
|
import type { ReactNode } from 'react';
|
|
@@ -37,9 +41,16 @@ export type ChatThreadManagerLike = {
|
|
|
37
41
|
deleteSession: () => void;
|
|
38
42
|
createSession: () => void;
|
|
39
43
|
goToProviders: () => void;
|
|
44
|
+
openChildSessionPanel: (params: {
|
|
45
|
+
parentSessionKey: string;
|
|
46
|
+
activeChildSessionKey?: string | null;
|
|
47
|
+
}) => void;
|
|
48
|
+
openFilePreview: (action: ChatFileOpenActionViewModel) => void;
|
|
40
49
|
openSessionFromToolAction: (action: ChatToolActionViewModel) => void;
|
|
41
50
|
selectChildSessionDetail: (sessionKey: string) => void;
|
|
42
|
-
|
|
51
|
+
selectWorkspaceFile: (fileKey: string) => void;
|
|
52
|
+
closeWorkspaceFile: (fileKey: string) => void;
|
|
53
|
+
closeWorkspacePanel: () => void;
|
|
43
54
|
goToParentSession: () => void;
|
|
44
55
|
};
|
|
45
56
|
|
|
@@ -6,6 +6,7 @@ import { ChatSessionHeaderActions } from '@/components/chat/session-header/chat-
|
|
|
6
6
|
const mocks = vi.hoisted(() => ({
|
|
7
7
|
updateSessionProject: vi.fn(),
|
|
8
8
|
onDeleteSession: vi.fn(),
|
|
9
|
+
onOpenChildSessions: vi.fn(),
|
|
9
10
|
}));
|
|
10
11
|
|
|
11
12
|
vi.mock('@/components/chat/hooks/use-chat-session-project', () => ({
|
|
@@ -20,6 +21,7 @@ describe('ChatSessionHeaderActions', () => {
|
|
|
20
21
|
beforeEach(() => {
|
|
21
22
|
mocks.updateSessionProject.mockReset();
|
|
22
23
|
mocks.onDeleteSession.mockReset();
|
|
24
|
+
mocks.onOpenChildSessions.mockReset();
|
|
23
25
|
});
|
|
24
26
|
|
|
25
27
|
it('keeps only the set-project action in the more-actions menu when a project is already attached', async () => {
|
|
@@ -31,6 +33,8 @@ describe('ChatSessionHeaderActions', () => {
|
|
|
31
33
|
canDeleteSession
|
|
32
34
|
isDeletePending={false}
|
|
33
35
|
projectRoot="/tmp/project-alpha"
|
|
36
|
+
childSessionCount={0}
|
|
37
|
+
onOpenChildSessions={mocks.onOpenChildSessions}
|
|
34
38
|
onDeleteSession={mocks.onDeleteSession}
|
|
35
39
|
/>
|
|
36
40
|
);
|
|
@@ -51,6 +55,8 @@ describe('ChatSessionHeaderActions', () => {
|
|
|
51
55
|
canDeleteSession={false}
|
|
52
56
|
isDeletePending={false}
|
|
53
57
|
projectRoot={null}
|
|
58
|
+
childSessionCount={0}
|
|
59
|
+
onOpenChildSessions={mocks.onOpenChildSessions}
|
|
54
60
|
onDeleteSession={mocks.onDeleteSession}
|
|
55
61
|
/>
|
|
56
62
|
);
|
|
@@ -60,4 +66,24 @@ describe('ChatSessionHeaderActions', () => {
|
|
|
60
66
|
expect(screen.getByText('Set Project Directory')).toBeTruthy();
|
|
61
67
|
expect(screen.queryByText('Clear Project Directory')).toBeNull();
|
|
62
68
|
});
|
|
69
|
+
|
|
70
|
+
it('shows a dedicated child-session entry button when the current session has child sessions', async () => {
|
|
71
|
+
const user = userEvent.setup();
|
|
72
|
+
|
|
73
|
+
render(
|
|
74
|
+
<ChatSessionHeaderActions
|
|
75
|
+
sessionKey="session-children"
|
|
76
|
+
canDeleteSession
|
|
77
|
+
isDeletePending={false}
|
|
78
|
+
projectRoot={null}
|
|
79
|
+
childSessionCount={2}
|
|
80
|
+
onOpenChildSessions={mocks.onOpenChildSessions}
|
|
81
|
+
onDeleteSession={mocks.onDeleteSession}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
await user.click(screen.getByRole('button', { name: 'View child sessions' }));
|
|
86
|
+
|
|
87
|
+
expect(mocks.onOpenChildSessions).toHaveBeenCalledTimes(1);
|
|
88
|
+
});
|
|
63
89
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import { FolderOpen, MoreHorizontal, Trash2 } from 'lucide-react';
|
|
2
|
+
import { FolderOpen, GitBranch, MoreHorizontal, Trash2 } from 'lucide-react';
|
|
3
3
|
import { Button } from '@/components/ui/button';
|
|
4
4
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
5
5
|
import { useChatSessionProject } from '@/components/chat/hooks/use-chat-session-project';
|
|
@@ -12,6 +12,8 @@ type ChatSessionHeaderActionsProps = {
|
|
|
12
12
|
canDeleteSession: boolean;
|
|
13
13
|
isDeletePending: boolean;
|
|
14
14
|
projectRoot?: string | null;
|
|
15
|
+
childSessionCount?: number;
|
|
16
|
+
onOpenChildSessions?: () => void;
|
|
15
17
|
onDeleteSession: () => void;
|
|
16
18
|
};
|
|
17
19
|
|
|
@@ -20,6 +22,8 @@ export function ChatSessionHeaderActions({
|
|
|
20
22
|
canDeleteSession,
|
|
21
23
|
isDeletePending,
|
|
22
24
|
projectRoot,
|
|
25
|
+
childSessionCount = 0,
|
|
26
|
+
onOpenChildSessions,
|
|
23
27
|
onDeleteSession,
|
|
24
28
|
}: ChatSessionHeaderActionsProps) {
|
|
25
29
|
const updateSessionProject = useChatSessionProject();
|
|
@@ -46,6 +50,20 @@ export function ChatSessionHeaderActions({
|
|
|
46
50
|
|
|
47
51
|
return (
|
|
48
52
|
<>
|
|
53
|
+
{childSessionCount > 0 && onOpenChildSessions ? (
|
|
54
|
+
<Button
|
|
55
|
+
type="button"
|
|
56
|
+
variant="ghost"
|
|
57
|
+
size="icon"
|
|
58
|
+
className="rounded-lg shrink-0 text-gray-400 hover:text-gray-700"
|
|
59
|
+
aria-label={t('chatSessionOpenChildSessions')}
|
|
60
|
+
title={t('chatSessionOpenChildSessions')}
|
|
61
|
+
onClick={onOpenChildSessions}
|
|
62
|
+
disabled={isBusy}
|
|
63
|
+
>
|
|
64
|
+
<GitBranch className="h-4 w-4" />
|
|
65
|
+
</Button>
|
|
66
|
+
) : null}
|
|
49
67
|
<Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
|
|
50
68
|
<PopoverTrigger asChild>
|
|
51
69
|
<Button
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
2
|
import type { MutableRefObject } from 'react';
|
|
3
3
|
import type { NcpMessage } from '@nextclaw/ncp';
|
|
4
|
+
import type { ChatFileOperationLineViewModel } from '@nextclaw/agent-chat-ui';
|
|
4
5
|
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
5
6
|
import type { AgentProfileView } from '@/api/types';
|
|
6
7
|
|
|
@@ -11,6 +12,23 @@ export type ChatChildSessionTab = {
|
|
|
11
12
|
agentId?: string | null;
|
|
12
13
|
};
|
|
13
14
|
|
|
15
|
+
export type ChatWorkspaceFileTab = {
|
|
16
|
+
key: string;
|
|
17
|
+
parentSessionKey: string | null;
|
|
18
|
+
path: string;
|
|
19
|
+
label?: string | null;
|
|
20
|
+
viewMode: 'preview' | 'diff';
|
|
21
|
+
line?: number | null;
|
|
22
|
+
column?: number | null;
|
|
23
|
+
rawText?: string | null;
|
|
24
|
+
beforeText?: string | null;
|
|
25
|
+
afterText?: string | null;
|
|
26
|
+
patchText?: string | null;
|
|
27
|
+
oldStartLine?: number | null;
|
|
28
|
+
newStartLine?: number | null;
|
|
29
|
+
fullLines?: ChatFileOperationLineViewModel[];
|
|
30
|
+
};
|
|
31
|
+
|
|
14
32
|
export type ChatThreadSnapshot = {
|
|
15
33
|
isProviderStateResolved: boolean;
|
|
16
34
|
modelOptions: ChatModelOption[];
|
|
@@ -34,8 +52,11 @@ export type ChatThreadSnapshot = {
|
|
|
34
52
|
isAwaitingAssistantOutput: boolean;
|
|
35
53
|
parentSessionKey?: string | null;
|
|
36
54
|
parentSessionLabel?: string | null;
|
|
55
|
+
workspacePanelParentKey?: string | null;
|
|
37
56
|
childSessionTabs: ChatChildSessionTab[];
|
|
38
57
|
activeChildSessionKey?: string | null;
|
|
58
|
+
workspaceFileTabs: ChatWorkspaceFileTab[];
|
|
59
|
+
activeWorkspaceFileKey?: string | null;
|
|
39
60
|
};
|
|
40
61
|
|
|
41
62
|
type ChatThreadStore = {
|
|
@@ -66,8 +87,11 @@ const initialSnapshot: ChatThreadSnapshot = {
|
|
|
66
87
|
isAwaitingAssistantOutput: false,
|
|
67
88
|
parentSessionKey: null,
|
|
68
89
|
parentSessionLabel: null,
|
|
90
|
+
workspacePanelParentKey: null,
|
|
69
91
|
childSessionTabs: [],
|
|
70
92
|
activeChildSessionKey: null,
|
|
93
|
+
workspaceFileTabs: [],
|
|
94
|
+
activeWorkspaceFileKey: null,
|
|
71
95
|
};
|
|
72
96
|
|
|
73
97
|
export const useChatThreadStore = create<ChatThreadStore>((set) => ({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { useConfig, useConfigSchema, useUpdateRuntime } from '@/hooks/useConfig';
|
|
3
|
-
import type { AgentBindingView, AgentProfileView } from '@/api/types';
|
|
3
|
+
import type { AgentBindingView, AgentProfileView, RuntimeEntryView } from '@/api/types';
|
|
4
4
|
import { Button } from '@/components/ui/button';
|
|
5
5
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
6
6
|
import { RuntimeControlCard } from '@/components/config/runtime-control-card';
|
|
@@ -19,11 +19,16 @@ import {
|
|
|
19
19
|
import { hintForPath } from '@/lib/config-hints';
|
|
20
20
|
import { t } from '@/lib/i18n';
|
|
21
21
|
import { PageLayout, PageHeader } from '@/components/layout/page-layout';
|
|
22
|
+
import { PwaInstallCard } from '@/pwa/components/pwa-install-entry';
|
|
22
23
|
import { Plus, Save, Trash2 } from 'lucide-react';
|
|
23
24
|
import { toast } from 'sonner';
|
|
24
25
|
|
|
25
26
|
type DmScope = 'main' | 'per-peer' | 'per-channel-peer' | 'per-account-channel-peer';
|
|
26
27
|
type PeerKind = '' | 'direct' | 'group' | 'channel';
|
|
28
|
+
type RuntimeEntryDraft = RuntimeEntryView & {
|
|
29
|
+
id: string;
|
|
30
|
+
configText: string;
|
|
31
|
+
};
|
|
27
32
|
|
|
28
33
|
const DM_SCOPE_OPTIONS: Array<{ value: DmScope; label: string }> = [
|
|
29
34
|
{ value: 'main', label: 'main' },
|
|
@@ -32,12 +37,25 @@ const DM_SCOPE_OPTIONS: Array<{ value: DmScope; label: string }> = [
|
|
|
32
37
|
{ value: 'per-account-channel-peer', label: 'per-account-channel-peer' }
|
|
33
38
|
];
|
|
34
39
|
|
|
40
|
+
const DEFAULT_NARP_STDIO_ENTRY_CONFIG = {
|
|
41
|
+
wireDialect: 'acp',
|
|
42
|
+
processScope: 'per-session',
|
|
43
|
+
command: '',
|
|
44
|
+
args: ['acp'],
|
|
45
|
+
env: {},
|
|
46
|
+
cwd: '',
|
|
47
|
+
startupTimeoutMs: 8000,
|
|
48
|
+
probeTimeoutMs: 3000,
|
|
49
|
+
requestTimeoutMs: 120000
|
|
50
|
+
};
|
|
51
|
+
|
|
35
52
|
function RuntimeConfigOverview() {
|
|
36
53
|
return (
|
|
37
54
|
<>
|
|
38
55
|
<PageHeader title={t('runtimePageTitle')} description={t('runtimePageDescription')} />
|
|
39
56
|
<RuntimeControlCard />
|
|
40
57
|
<RuntimePresenceCard />
|
|
58
|
+
<PwaInstallCard />
|
|
41
59
|
</>
|
|
42
60
|
);
|
|
43
61
|
}
|
|
@@ -49,6 +67,7 @@ export function RuntimeConfig() {
|
|
|
49
67
|
|
|
50
68
|
const [agents, setAgents] = useState<AgentProfileView[]>([]);
|
|
51
69
|
const [bindings, setBindings] = useState<AgentBindingView[]>([]);
|
|
70
|
+
const [runtimeEntries, setRuntimeEntries] = useState<RuntimeEntryDraft[]>([]);
|
|
52
71
|
const [dmScope, setDmScope] = useState<DmScope>('per-channel-peer');
|
|
53
72
|
const [defaultContextTokens, setDefaultContextTokens] = useState(200000);
|
|
54
73
|
const [defaultEngine, setDefaultEngine] = useState('native');
|
|
@@ -59,6 +78,16 @@ export function RuntimeConfig() {
|
|
|
59
78
|
}
|
|
60
79
|
setAgents((config.agents.list ?? []).map(hydrateRuntimeAgent));
|
|
61
80
|
setBindings((config.bindings ?? []).map(hydrateRuntimeBinding));
|
|
81
|
+
setRuntimeEntries(
|
|
82
|
+
Object.entries(config.agents.runtimes?.entries ?? {}).map(([id, entry]) => ({
|
|
83
|
+
id,
|
|
84
|
+
enabled: entry.enabled !== false,
|
|
85
|
+
label: entry.label ?? '',
|
|
86
|
+
type: entry.type,
|
|
87
|
+
config: entry.config ?? {},
|
|
88
|
+
configText: JSON.stringify(entry.config ?? {}, null, 2)
|
|
89
|
+
}))
|
|
90
|
+
);
|
|
62
91
|
setDmScope((config.session?.dmScope as DmScope) ?? 'per-channel-peer');
|
|
63
92
|
setDefaultContextTokens(config.agents.defaults.contextTokens ?? 200000);
|
|
64
93
|
setDefaultEngine(config.agents.defaults.engine ?? 'native');
|
|
@@ -72,6 +101,7 @@ export function RuntimeConfig() {
|
|
|
72
101
|
const agentEngineHint = hintForPath('agents.list.*.engine', uiHints);
|
|
73
102
|
const agentsHint = hintForPath('agents.list', uiHints);
|
|
74
103
|
const bindingsHint = hintForPath('bindings', uiHints);
|
|
104
|
+
const runtimeEntriesHint = hintForPath('agents.runtimes.entries', uiHints);
|
|
75
105
|
|
|
76
106
|
const knownAgentIds = useMemo(() => {
|
|
77
107
|
const ids = new Set<string>(['main']);
|
|
@@ -91,6 +121,29 @@ export function RuntimeConfig() {
|
|
|
91
121
|
const updateBinding = (index: number, next: AgentBindingView) => {
|
|
92
122
|
setBindings((prev) => prev.map((binding, cursor) => (cursor === index ? next : binding)));
|
|
93
123
|
};
|
|
124
|
+
|
|
125
|
+
const updateRuntimeEntry = (index: number, patch: Partial<RuntimeEntryDraft>) => {
|
|
126
|
+
setRuntimeEntries((prev) => prev.map((entry, cursor) => (cursor === index ? { ...entry, ...patch } : entry)));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const removeRuntimeEntry = (index: number) => {
|
|
130
|
+
setRuntimeEntries((prev) => prev.filter((_, cursor) => cursor !== index));
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const addRuntimeEntry = () => {
|
|
134
|
+
setRuntimeEntries((prev) => [
|
|
135
|
+
...prev,
|
|
136
|
+
{
|
|
137
|
+
id: '',
|
|
138
|
+
enabled: true,
|
|
139
|
+
label: '',
|
|
140
|
+
type: 'narp-stdio',
|
|
141
|
+
config: DEFAULT_NARP_STDIO_ENTRY_CONFIG,
|
|
142
|
+
configText: JSON.stringify(DEFAULT_NARP_STDIO_ENTRY_CONFIG, null, 2)
|
|
143
|
+
}
|
|
144
|
+
]);
|
|
145
|
+
};
|
|
146
|
+
|
|
94
147
|
const handleSave = () => {
|
|
95
148
|
try {
|
|
96
149
|
const normalizedAgents = agents.map((agent, index) => {
|
|
@@ -150,6 +203,33 @@ export function RuntimeConfig() {
|
|
|
150
203
|
return normalized;
|
|
151
204
|
});
|
|
152
205
|
|
|
206
|
+
const normalizedRuntimeEntries = runtimeEntries.reduce<Record<string, RuntimeEntryView>>((entries, entry, index) => {
|
|
207
|
+
const id = entry.id.trim();
|
|
208
|
+
const type = entry.type.trim();
|
|
209
|
+
if (!id) {
|
|
210
|
+
throw new Error(`Runtime entry id is required at index ${index}.`);
|
|
211
|
+
}
|
|
212
|
+
if (!type) {
|
|
213
|
+
throw new Error(`Runtime entry type is required for "${id}".`);
|
|
214
|
+
}
|
|
215
|
+
if (entries[id]) {
|
|
216
|
+
throw new Error(`Duplicate runtime entry id: ${id}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const configValue = entry.configText.trim() ? JSON.parse(entry.configText) : {};
|
|
220
|
+
if (configValue && (typeof configValue !== 'object' || Array.isArray(configValue))) {
|
|
221
|
+
throw new Error(`Runtime entry config for "${id}" must be a JSON object.`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
entries[id] = {
|
|
225
|
+
enabled: entry.enabled !== false,
|
|
226
|
+
...(entry.label?.trim() ? { label: entry.label.trim() } : {}),
|
|
227
|
+
type,
|
|
228
|
+
config: (configValue as Record<string, unknown>) ?? {}
|
|
229
|
+
};
|
|
230
|
+
return entries;
|
|
231
|
+
}, {});
|
|
232
|
+
|
|
153
233
|
updateRuntime.mutate({
|
|
154
234
|
data: {
|
|
155
235
|
agents: {
|
|
@@ -157,7 +237,10 @@ export function RuntimeConfig() {
|
|
|
157
237
|
contextTokens: Math.max(1000, defaultContextTokens),
|
|
158
238
|
engine: defaultEngine.trim() || 'native'
|
|
159
239
|
},
|
|
160
|
-
list: normalizedAgents
|
|
240
|
+
list: normalizedAgents,
|
|
241
|
+
runtimes: {
|
|
242
|
+
entries: normalizedRuntimeEntries
|
|
243
|
+
}
|
|
161
244
|
},
|
|
162
245
|
bindings: normalizedBindings,
|
|
163
246
|
session: {
|
|
@@ -228,6 +311,62 @@ export function RuntimeConfig() {
|
|
|
228
311
|
</CardContent>
|
|
229
312
|
</Card>
|
|
230
313
|
|
|
314
|
+
<Card>
|
|
315
|
+
<CardHeader>
|
|
316
|
+
<CardTitle>{runtimeEntriesHint?.label ?? 'Runtime Entries'}</CardTitle>
|
|
317
|
+
<CardDescription>{runtimeEntriesHint?.help ?? '统一管理可见的 runtime entry 与其配置。'}</CardDescription>
|
|
318
|
+
</CardHeader>
|
|
319
|
+
<CardContent className="space-y-3">
|
|
320
|
+
{runtimeEntries.map((entry, index) => (
|
|
321
|
+
<div key={`${index}-${entry.id || 'runtime-entry'}`} className="rounded-xl border border-gray-200 p-3 space-y-3">
|
|
322
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
323
|
+
<Input
|
|
324
|
+
value={entry.id}
|
|
325
|
+
onChange={(event) => updateRuntimeEntry(index, { id: event.target.value })}
|
|
326
|
+
placeholder="entry id,例如 hermes"
|
|
327
|
+
/>
|
|
328
|
+
<Input
|
|
329
|
+
value={entry.label ?? ''}
|
|
330
|
+
onChange={(event) => updateRuntimeEntry(index, { label: event.target.value })}
|
|
331
|
+
placeholder="展示名称,例如 Hermes"
|
|
332
|
+
/>
|
|
333
|
+
<Input
|
|
334
|
+
value={entry.type}
|
|
335
|
+
onChange={(event) => updateRuntimeEntry(index, { type: event.target.value })}
|
|
336
|
+
placeholder="runtime type,例如 narp-stdio"
|
|
337
|
+
/>
|
|
338
|
+
<div className="flex items-center justify-between rounded-lg border border-gray-200 px-3 py-2">
|
|
339
|
+
<span className="text-sm text-gray-700">Enabled</span>
|
|
340
|
+
<Switch
|
|
341
|
+
checked={entry.enabled !== false}
|
|
342
|
+
onCheckedChange={(checked) => updateRuntimeEntry(index, { enabled: checked })}
|
|
343
|
+
/>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
<div className="space-y-2">
|
|
347
|
+
<label className="text-sm font-medium text-gray-800">Config JSON</label>
|
|
348
|
+
<textarea
|
|
349
|
+
className="min-h-32 w-full rounded-md border border-gray-200 px-3 py-2 text-sm font-mono"
|
|
350
|
+
value={entry.configText}
|
|
351
|
+
onChange={(event) => updateRuntimeEntry(index, { configText: event.target.value })}
|
|
352
|
+
spellCheck={false}
|
|
353
|
+
/>
|
|
354
|
+
</div>
|
|
355
|
+
<div className="flex justify-end">
|
|
356
|
+
<Button type="button" variant="outline" onClick={() => removeRuntimeEntry(index)}>
|
|
357
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
358
|
+
{t('deleteButton')}
|
|
359
|
+
</Button>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
))}
|
|
363
|
+
<Button type="button" variant="outline" onClick={addRuntimeEntry}>
|
|
364
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
365
|
+
Add Runtime Entry
|
|
366
|
+
</Button>
|
|
367
|
+
</CardContent>
|
|
368
|
+
</Card>
|
|
369
|
+
|
|
231
370
|
<Card>
|
|
232
371
|
<CardHeader>
|
|
233
372
|
<CardTitle>{agentsHint?.label ?? t('agentList')}</CardTitle>
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
subscribeThemeChange,
|
|
15
15
|
type UiTheme,
|
|
16
16
|
} from '@/lib/theme';
|
|
17
|
+
import { pwaShellThemeManager } from '@/pwa/managers/pwa-shell-theme.manager';
|
|
17
18
|
|
|
18
19
|
type ThemeContextValue = {
|
|
19
20
|
theme: UiTheme;
|
|
@@ -25,6 +26,10 @@ const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
|
25
26
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
26
27
|
const [theme, setThemeState] = useState<UiTheme>(() => initializeTheme());
|
|
27
28
|
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
pwaShellThemeManager.syncTheme(theme);
|
|
31
|
+
}, [theme]);
|
|
32
|
+
|
|
28
33
|
useEffect(() => {
|
|
29
34
|
const unsubscribe = subscribeThemeChange((nextTheme) => {
|
|
30
35
|
setThemeState(nextTheme);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { fetchServerPathRead } from '@/api/server-path';
|
|
3
|
+
|
|
4
|
+
export function useServerPathRead(params: {
|
|
5
|
+
path?: string | null;
|
|
6
|
+
basePath?: string | null;
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
}) {
|
|
9
|
+
const normalizedPath = params.path?.trim() ?? '';
|
|
10
|
+
return useQuery({
|
|
11
|
+
queryKey: ['server-path-read', normalizedPath, params.basePath ?? null],
|
|
12
|
+
queryFn: () =>
|
|
13
|
+
fetchServerPathRead({
|
|
14
|
+
path: normalizedPath,
|
|
15
|
+
basePath: params.basePath,
|
|
16
|
+
}),
|
|
17
|
+
enabled: (params.enabled ?? true) && normalizedPath.length > 0,
|
|
18
|
+
staleTime: 0,
|
|
19
|
+
});
|
|
20
|
+
}
|
package/src/lib/chat-message.ts
CHANGED
|
@@ -24,6 +24,17 @@ function truncateText(value: string, maxChars = 2400): string {
|
|
|
24
24
|
return `${value.slice(0, maxChars)}\n…`;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function truncateInlineText(value: string, maxChars = 120): string {
|
|
28
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
29
|
+
if (normalized.length <= maxChars) {
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
if (maxChars <= 1) {
|
|
33
|
+
return '…';
|
|
34
|
+
}
|
|
35
|
+
return `${normalized.slice(0, maxChars - 1)}…`;
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
export function stringifyUnknown(value: unknown): string {
|
|
28
39
|
if (typeof value === 'string') {
|
|
29
40
|
return value;
|
|
@@ -64,7 +75,7 @@ export function summarizeToolArgs(args: unknown): string | undefined {
|
|
|
64
75
|
const parsed = parseArgsObject(args);
|
|
65
76
|
if (!parsed) {
|
|
66
77
|
const text = stringifyUnknown(args).trim();
|
|
67
|
-
return text ?
|
|
78
|
+
return text ? truncateInlineText(text, 120) : undefined;
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
const items: string[] = [];
|
|
@@ -80,9 +91,9 @@ export function summarizeToolArgs(args: unknown): string | undefined {
|
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
93
|
if (items.length > 0) {
|
|
83
|
-
return items.join(' · ');
|
|
94
|
+
return truncateInlineText(items.join(' · '), 120);
|
|
84
95
|
}
|
|
85
|
-
return
|
|
96
|
+
return truncateInlineText(stringifyUnknown(parsed), 140);
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
function toToolName(value: unknown): string {
|
package/src/lib/i18n.chat.ts
CHANGED
|
@@ -43,9 +43,20 @@ export const CHAT_LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
43
43
|
chatHistoryLoading: { zh: '加载会话历史中...', en: 'Loading session history...' },
|
|
44
44
|
chatNoMessages: { zh: '暂无消息,发送一条开始对话。', en: 'No messages yet. Send one to start.' },
|
|
45
45
|
chatBackToParent: { zh: '返回父会话', en: 'Back to parent' },
|
|
46
|
+
chatSessionOpenChildSessions: { zh: '查看子会话', en: 'View child sessions' },
|
|
46
47
|
chatChildSessionLoading: { zh: '正在加载子会话…', en: 'Loading child session…' },
|
|
47
48
|
chatChildSessionEmpty: { zh: '子会话还没有消息。', en: 'No child session messages yet.' },
|
|
48
|
-
|
|
49
|
+
chatWorkspaceClosePanel: { zh: '关闭工作区侧栏', en: 'Close workspace panel' },
|
|
50
|
+
chatWorkspaceChildSessions: { zh: '子会话', en: 'Child sessions' },
|
|
51
|
+
chatWorkspaceOpenFiles: { zh: '打开的文件', en: 'Open files' },
|
|
52
|
+
chatWorkspacePreview: { zh: '预览', en: 'Preview' },
|
|
53
|
+
chatWorkspaceDiff: { zh: 'Diff', en: 'Diff' },
|
|
54
|
+
chatWorkspaceCloseFile: { zh: '关闭文件', en: 'Close file' },
|
|
55
|
+
chatWorkspaceLoadingFile: { zh: '正在加载文件…', en: 'Loading file…' },
|
|
56
|
+
chatWorkspacePreviewUnsupported: { zh: '该文件暂不支持在侧栏预览。', en: 'This file is not supported in the sidebar preview yet.' },
|
|
57
|
+
chatWorkspacePreviewEmpty: { zh: '当前没有可显示的文件内容。', en: 'No file content is available for preview.' },
|
|
58
|
+
chatWorkspaceDiffEmpty: { zh: '当前没有可显示的 diff 内容。', en: 'No diff content is available.' },
|
|
59
|
+
chatWorkspacePreviewTruncated: { zh: '内容已截断', en: 'Preview truncated' },
|
|
49
60
|
chatSessionUnread: { zh: '会话有未读更新', en: 'Session has unread updates' },
|
|
50
61
|
chatTyping: { zh: 'Agent 正在思考...', en: 'Agent is thinking...' },
|
|
51
62
|
chatInputPlaceholder: { zh: '输入消息,输入 / 选择技能,Enter 发送,Shift + Enter 换行', en: 'Type a message, type / to select skills, Enter to send, Shift + Enter for newline' },
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const PWA_LABELS: Record<string, { zh: string; en: string }> = {
|
|
2
|
+
pwaInstallTitle: { zh: '安装为应用', en: 'Install as App' },
|
|
3
|
+
pwaInstallDescription: {
|
|
4
|
+
zh: '把当前 NextClaw UI 安装为独立入口,方便从桌面、启动器或任务栏直接打开。',
|
|
5
|
+
en: 'Install this NextClaw UI as a standalone entry point you can launch from your desktop, launcher, or dock.'
|
|
6
|
+
},
|
|
7
|
+
pwaInstallAction: { zh: '安装 NextClaw', en: 'Install NextClaw' },
|
|
8
|
+
pwaInstallDismiss: { zh: '不再提示', en: "Don't Ask Again" },
|
|
9
|
+
pwaInstallAccepted: { zh: '已打开安装面板。', en: 'Install prompt opened.' },
|
|
10
|
+
pwaInstalledToast: { zh: 'NextClaw 已安装为应用入口。', en: 'NextClaw is now installed as an app.' },
|
|
11
|
+
pwaInstallStatusAvailable: { zh: '可安装', en: 'Installable' },
|
|
12
|
+
pwaInstallStatusInstalled: { zh: '已安装', en: 'Installed' },
|
|
13
|
+
pwaInstallStatusDesktopHost: { zh: '桌面宿主已接管', en: 'Desktop Host Active' },
|
|
14
|
+
pwaInstallStatusUnavailable: { zh: '当前不可安装', en: 'Unavailable' },
|
|
15
|
+
pwaInstallCardPrompt: {
|
|
16
|
+
zh: '当前浏览器已经准备好安装面板。安装后,NextClaw 会以独立窗口形态打开,但仍沿用同一套 Web UI 和运行时连接逻辑。',
|
|
17
|
+
en: 'Your browser is ready to install NextClaw. Once installed, it opens in a standalone window while keeping the same Web UI and runtime behavior.'
|
|
18
|
+
},
|
|
19
|
+
pwaInstallCardManual: {
|
|
20
|
+
zh: '当前环境支持把 NextClaw 安装为应用,但浏览器没有提供即时安装弹窗。你仍可通过浏览器菜单中的“安装应用”或“添加到主屏幕”完成安装。',
|
|
21
|
+
en: 'This environment can install NextClaw as an app, but the browser did not expose an immediate install prompt. Use your browser menu to install or add it to the home screen.'
|
|
22
|
+
},
|
|
23
|
+
pwaInstallCardInstalled: {
|
|
24
|
+
zh: '当前已经以应用形态运行。浏览器访问与已安装形态共用同一套 NextClaw UI,不会分叉成第二套产品逻辑。',
|
|
25
|
+
en: 'NextClaw is already running as an installed app. Browser access and the installed experience share the same UI and product behavior.'
|
|
26
|
+
},
|
|
27
|
+
pwaInstallCardSuppressed: {
|
|
28
|
+
zh: '当前 UI 已运行在 Electron 桌面宿主中,原生桌面壳优先于 PWA 入口,因此这里不会继续展示安装入口。',
|
|
29
|
+
en: 'This UI is already running inside the Electron desktop host. The native desktop shell takes priority, so the PWA install entry stays hidden here.'
|
|
30
|
+
},
|
|
31
|
+
pwaInstallCardInsecureContext: {
|
|
32
|
+
zh: '当前地址不是浏览器允许安装 PWA 的安全上下文。请使用 `localhost`、`127.0.0.1` 或 HTTPS 域名访问。',
|
|
33
|
+
en: 'This address is not a secure context for browser-managed app installation. Use localhost, 127.0.0.1, or an HTTPS origin instead.'
|
|
34
|
+
},
|
|
35
|
+
pwaInstallCardDevServer: {
|
|
36
|
+
zh: '当前是 Vite 开发环境。为避免 service worker 缓存和 HMR 热更新互相干扰,开发态默认关闭 PWA 安装与更新能力;请使用 preview 或正式构建验证 PWA。',
|
|
37
|
+
en: 'This is the Vite development server. PWA install and update are disabled in dev to avoid service worker caching conflicts with HMR; use preview or a production build to verify the PWA.'
|
|
38
|
+
},
|
|
39
|
+
pwaInstallCardUnsupported: {
|
|
40
|
+
zh: '当前浏览器环境不支持这套安装能力,或缺少 PWA 所需的关键运行能力。',
|
|
41
|
+
en: 'This browser environment does not support the required installation capabilities for this PWA shell.'
|
|
42
|
+
},
|
|
43
|
+
pwaInstallPromptHint: {
|
|
44
|
+
zh: '安装后仍然连接当前本地或远端 NextClaw 服务,不会额外生成一套离线副本。',
|
|
45
|
+
en: 'The installed app still connects to the same local or remote NextClaw service instead of creating an offline copy.'
|
|
46
|
+
},
|
|
47
|
+
pwaInstallManualHint: {
|
|
48
|
+
zh: '如果浏览器没有弹出安装面板,请打开浏览器菜单,选择“安装应用”“安装此站点”或“添加到主屏幕”等同类入口。',
|
|
49
|
+
en: 'If the browser does not show an install prompt, open the browser menu and look for actions such as Install App, Install Site, or Add to Home Screen.'
|
|
50
|
+
},
|
|
51
|
+
pwaInstallBannerTitle: { zh: '把 NextClaw 固定成桌面入口', en: 'Pin NextClaw as an App' },
|
|
52
|
+
pwaInstallBannerDescription: {
|
|
53
|
+
zh: '当前站点已经满足安装条件。安装后你可以像打开普通应用一样直接进入 NextClaw。',
|
|
54
|
+
en: 'This site is ready to install. Once installed, you can launch NextClaw like a regular app.'
|
|
55
|
+
},
|
|
56
|
+
pwaUpdateBannerTitle: { zh: 'NextClaw 已准备好更新', en: 'NextClaw Update Ready' },
|
|
57
|
+
pwaUpdateBannerDescription: {
|
|
58
|
+
zh: '检测到新的 PWA 壳版本,刷新后即可切换到最新 UI 资源。',
|
|
59
|
+
en: 'A newer PWA shell version is ready. Refresh to switch to the latest UI assets.'
|
|
60
|
+
},
|
|
61
|
+
pwaUpdateAction: { zh: '刷新更新', en: 'Refresh Now' }
|
|
62
|
+
};
|
package/src/lib/i18n.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { DESKTOP_UPDATE_LABELS } from './desktop-update-labels.utils';
|
|
16
16
|
import { MARKETPLACE_LABELS } from './i18n.marketplace';
|
|
17
17
|
import { PATH_PICKER_LABELS } from './i18n-runtime/i18n.path-picker';
|
|
18
|
+
import { PWA_LABELS } from './i18n.pwa';
|
|
18
19
|
import { REMOTE_LABELS } from './i18n.remote';
|
|
19
20
|
import { RUNTIME_CONTROL_LABELS } from './i18n.runtime-control';
|
|
20
21
|
import { SEARCH_LABELS } from './i18n.search';
|
|
@@ -24,7 +25,6 @@ export function formatDateTime(value?: string | Date, lang: I18nLanguage = getLa
|
|
|
24
25
|
if (!value) {
|
|
25
26
|
return '-';
|
|
26
27
|
}
|
|
27
|
-
|
|
28
28
|
const date = value instanceof Date ? value : new Date(value);
|
|
29
29
|
if (Number.isNaN(date.getTime())) {
|
|
30
30
|
return typeof value === 'string' ? value : '-';
|
|
@@ -517,7 +517,6 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
517
517
|
enterTag: { zh: '输入后按回车...', en: 'Type and press Enter...' },
|
|
518
518
|
headerName: { zh: 'Header 名称', en: 'Header Name' },
|
|
519
519
|
headerValue: { zh: 'Header 值', en: 'Header Value' },
|
|
520
|
-
|
|
521
520
|
// Doc Browser
|
|
522
521
|
docBrowserTitle: { zh: '内嵌浏览器', en: 'Embedded Browser' },
|
|
523
522
|
docBrowserSearchPlaceholder: { zh: '搜索,也可以输入文档地址直接打开', en: 'Search, or enter a doc URL to open' },
|
|
@@ -531,6 +530,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
531
530
|
docBrowserCloseTab: { zh: '关闭标签', en: 'Close Tab' },
|
|
532
531
|
docBrowserTabUntitled: { zh: '未命名', en: 'Untitled' },
|
|
533
532
|
...PATH_PICKER_LABELS,
|
|
533
|
+
...PWA_LABELS,
|
|
534
534
|
...CHANNEL_AUTH_LABELS,
|
|
535
535
|
};
|
|
536
536
|
|