@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.
Files changed (142) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/assets/ChannelsList-Ita2Zm1_.js +8 -0
  3. package/dist/assets/{DocBrowser-BMxf9CIK.js → DocBrowser-6ReNjvzF.js} +1 -1
  4. package/dist/assets/DocBrowser-BNwbPHf4.js +1 -0
  5. package/dist/assets/{DocBrowserContext-Ce28gRXt.js → DocBrowserContext-B6SpA7Qs.js} +1 -1
  6. package/dist/assets/{LogoBadge-o92MOA2L.js → LogoBadge-ByNLYg65.js} +1 -1
  7. package/dist/assets/MarketplacePage-CjX2MWww.js +1 -0
  8. package/dist/assets/{MarketplacePage-BySqkYDh.js → MarketplacePage-D0sDlYX4.js} +1 -1
  9. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +40 -0
  10. package/dist/assets/{ModelConfig-IrmzoslW.js → ModelConfig-BzZenCH-.js} +1 -1
  11. package/dist/assets/{ProviderScopedModelInput-CmTIzgI7.js → ProviderScopedModelInput-Da7khnBA.js} +1 -1
  12. package/dist/assets/{ProvidersList-8_Kalfwl.js → ProvidersList-BbVzRxjY.js} +1 -1
  13. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +1 -0
  14. package/dist/assets/RuntimeConfig-F_XKGgLm.js +1 -0
  15. package/dist/assets/{SearchConfig-DNBR-UbE.js → SearchConfig-BGkzXQP-.js} +1 -1
  16. package/dist/assets/{SecretsConfig-Ba1RPJaG.js → SecretsConfig-D281Rotl.js} +2 -2
  17. package/dist/assets/{SessionsConfig-Doqp5ghH.js → SessionsConfig-ChHQ7M5c.js} +2 -2
  18. package/dist/assets/{app-query-client-DniXoIN5.js → app-query-client-VnFElj4E.js} +1 -1
  19. package/dist/assets/{book-open-DocgeQtR.js → book-open-BdcxxoQu.js} +1 -1
  20. package/dist/assets/chat-page-Doe0yTtB.js +58 -0
  21. package/dist/assets/chat-session-display-cw78aiI_.js +1 -0
  22. package/dist/assets/{chunk-JZWAC4HX-BvKvh1R8.js → chunk-JZWAC4HX-DK5HPmIK.js} +1 -1
  23. package/dist/assets/{client-CVqPF5ie.js → client-_i4MU2bB.js} +1 -1
  24. package/dist/assets/{config-Bop2oB18.js → config-DtIQwrHF.js} +1 -1
  25. package/dist/assets/{createLucideIcon-DVv8taGY.js → createLucideIcon-BSeTgkZW.js} +1 -1
  26. package/dist/assets/desktop-update-config-Dpcf4BKG.js +1 -0
  27. package/dist/assets/{dist-Da5Gm_pO.js → dist-6TrrnPCR.js} +1 -1
  28. package/dist/assets/{dist-DmAlInRu.js → dist-ccBFUi-o.js} +1 -1
  29. package/dist/assets/download-BhDxnyvU.js +1 -0
  30. package/dist/assets/{external-link-DFjw3x1B.js → external-link-BgErLCNT.js} +1 -1
  31. package/dist/assets/{hash-DJtaCejM.js → hash-Bl7dr_UG.js} +1 -1
  32. package/dist/assets/i18n-eDHeDY0n.js +1 -0
  33. package/dist/assets/index-CF9xve0E.js +6 -0
  34. package/dist/assets/index-FgA52VBt.css +1 -0
  35. package/dist/assets/{infiniteQueryBehavior-DHSEQ3OH.js → infiniteQueryBehavior-ZDS92Qpp.js} +1 -1
  36. package/dist/assets/loader-circle-ACM1s51e.js +1 -0
  37. package/dist/assets/{logos-DEFUIR12.js → logos-x89HbrZ4.js} +1 -1
  38. package/dist/assets/{page-layout-Da3i3r6G.js → page-layout-vZnghcFy.js} +1 -1
  39. package/dist/assets/play-CFUwCA2E.js +1 -0
  40. package/dist/assets/plus-rYsv72JG.js +1 -0
  41. package/dist/assets/{popover-C_mWOFzI.js → popover-Bg1VoTZ6.js} +1 -1
  42. package/dist/assets/{refresh-ccw-D6HkNtfz.js → refresh-ccw-DT98i__E.js} +1 -1
  43. package/dist/assets/{refresh-cw-DRcvRrnc.js → refresh-cw-C47QSEwg.js} +1 -1
  44. package/dist/assets/{rotate-cw-BmDKfXtH.js → rotate-cw-JtFzpNn6.js} +1 -1
  45. package/dist/assets/{save-DHGmi2e9.js → save-3S6-H3Xw.js} +1 -1
  46. package/dist/assets/search-3kFR_zh9.js +1 -0
  47. package/dist/assets/{security-config-CbXfPZzr.js → security-config-BWaiARNk.js} +1 -1
  48. package/dist/assets/{select-Caud8QvU.js → select-DJ2MUjBB.js} +1 -1
  49. package/dist/assets/skeleton-ByQepn0M.js +1 -0
  50. package/dist/assets/{status-dot-DurKKSwA.js → status-dot-vbanNPFU.js} +1 -1
  51. package/dist/assets/{switch-0rmPBRKI.js → switch-BsLtHOH-.js} +1 -1
  52. package/dist/assets/{tabs-custom-5JLVL6v8.js → tabs-custom-D3HYMt6k.js} +1 -1
  53. package/dist/assets/{trash-2-C6caKPoz.js → trash-2-G48scll7.js} +1 -1
  54. package/dist/assets/{use-infinite-scroll-loader-dwnaa_qi.js → use-infinite-scroll-loader-DkNhD-42.js} +1 -1
  55. package/dist/assets/{useConfirmDialog-mMeWD_yo.js → useConfirmDialog-BkvTN-vd.js} +1 -1
  56. package/dist/assets/{useMutation-BmxxvCNf.js → useMutation-CBWjE2uj.js} +1 -1
  57. package/dist/assets/x-ByDbItbq.js +1 -0
  58. package/dist/index.html +95 -21
  59. package/dist/manifest.webmanifest +30 -0
  60. package/dist/offline.html +102 -0
  61. package/dist/pwa-192.png +0 -0
  62. package/dist/pwa-512.png +0 -0
  63. package/dist/sw.js +80 -0
  64. package/index.html +73 -1
  65. package/package.json +6 -6
  66. package/public/manifest.webmanifest +30 -0
  67. package/public/offline.html +102 -0
  68. package/public/pwa-192.png +0 -0
  69. package/public/pwa-512.png +0 -0
  70. package/public/sw.js +80 -0
  71. package/src/api/server-path.ts +27 -4
  72. package/src/api/types.ts +17 -10
  73. package/src/app.tsx +9 -0
  74. package/src/components/chat/ChatSidebar.test.tsx +43 -1
  75. package/src/components/chat/ChatSidebar.tsx +24 -0
  76. package/src/components/chat/adapters/chat-message.summary-truncation.test.ts +66 -0
  77. package/src/components/chat/adapters/file-operation/card.ts +9 -0
  78. package/src/components/chat/adapters/file-operation/diff.ts +14 -0
  79. package/src/components/chat/{ChatConversationPanel.test.tsx → chat-conversation-panel.test.tsx} +107 -206
  80. package/src/components/chat/chat-conversation-panel.tsx +412 -0
  81. package/src/components/chat/chat-page-shell.tsx +1 -1
  82. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +91 -0
  83. package/src/components/chat/chat-session-workspace-file-preview.tsx +307 -0
  84. package/src/components/chat/chat-session-workspace-panel-nav.tsx +197 -0
  85. package/src/components/chat/chat-session-workspace-panel.tsx +318 -0
  86. package/src/components/chat/chat-sidebar-session-item.tsx +32 -2
  87. package/src/components/chat/containers/chat-message-list.container.test.tsx +49 -0
  88. package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
  89. package/src/components/chat/managers/chat-session-list.manager.test.ts +12 -0
  90. package/src/components/chat/managers/chat-session-list.manager.ts +7 -0
  91. package/src/components/chat/ncp/ncp-chat-page.tsx +7 -7
  92. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +179 -41
  93. package/src/components/chat/ncp/ncp-session-adapter.test.ts +35 -1
  94. package/src/components/chat/ncp/ncp-session-adapter.ts +17 -0
  95. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +54 -11
  96. package/src/components/chat/ncp/tests/ncp-chat-thread.manager.test.ts +189 -0
  97. package/src/components/chat/presenter/chat-presenter-context.tsx +13 -2
  98. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +26 -0
  99. package/src/components/chat/session-header/chat-session-header-actions.tsx +19 -1
  100. package/src/components/chat/stores/chat-thread.store.ts +24 -0
  101. package/src/components/config/RuntimeConfig.tsx +141 -2
  102. package/src/components/layout/AppLayout.tsx +1 -1
  103. package/src/components/providers/ThemeProvider.tsx +5 -0
  104. package/src/hooks/server-path/use-server-path-read.ts +20 -0
  105. package/src/lib/chat-message.ts +14 -3
  106. package/src/lib/i18n.chat.ts +12 -1
  107. package/src/lib/i18n.pwa.ts +62 -0
  108. package/src/lib/i18n.ts +2 -2
  109. package/src/pwa/components/pwa-install-entry.test.tsx +110 -0
  110. package/src/pwa/components/pwa-install-entry.tsx +205 -0
  111. package/src/pwa/managers/pwa-install.manager.test.ts +160 -0
  112. package/src/pwa/managers/pwa-install.manager.ts +232 -0
  113. package/src/pwa/managers/pwa-runtime.manager.ts +196 -0
  114. package/src/pwa/managers/pwa-shell-theme.manager.test.ts +30 -0
  115. package/src/pwa/managers/pwa-shell-theme.manager.ts +46 -0
  116. package/src/pwa/pwa-install-banner.storage.ts +55 -0
  117. package/src/pwa/pwa.types.ts +22 -0
  118. package/src/pwa/register-pwa.ts +14 -0
  119. package/src/pwa/stores/pwa.store.ts +17 -0
  120. package/src/vite-env.d.ts +9 -0
  121. package/dist/assets/ChannelsList-KIQIxluX.js +0 -8
  122. package/dist/assets/DocBrowser-CyDgAtO9.js +0 -1
  123. package/dist/assets/MarketplacePage-C0olZaek.js +0 -1
  124. package/dist/assets/McpMarketplacePage-DqKaiXO9.js +0 -40
  125. package/dist/assets/RemoteAccessPage-CyQlSjPf.js +0 -1
  126. package/dist/assets/RuntimeConfig-Bk0uYBhf.js +0 -1
  127. package/dist/assets/chat-page-Bph8M5zo.js +0 -58
  128. package/dist/assets/chat-session-display-CoN3Wmn-.js +0 -1
  129. package/dist/assets/desktop-update-config-1KBrqLBC.js +0 -1
  130. package/dist/assets/i18n-CwHZ-9vt.js +0 -1
  131. package/dist/assets/index-DafCdM4F.css +0 -1
  132. package/dist/assets/index-DdksE6U3.js +0 -6
  133. package/dist/assets/loader-circle-PsSP0H9n.js +0 -1
  134. package/dist/assets/play-DBQbBxTA.js +0 -1
  135. package/dist/assets/plus-DUOVbsyQ.js +0 -1
  136. package/dist/assets/search-MChQRYR1.js +0 -1
  137. package/dist/assets/skeleton-B-4vRq_Z.js +0 -1
  138. package/dist/assets/x-DuMhMATD.js +0 -1
  139. package/src/components/chat/ChatConversationPanel.tsx +0 -256
  140. package/src/components/chat/chat-child-session-panel.tsx +0 -270
  141. /package/dist/assets/{config-hints-BZoDjXye.js → config-hints-BhTmc9P1.js} +0 -0
  142. /package/dist/assets/{config-layout-DmlGaay2.js → config-layout-CHs0mAaR.js} +0 -0
@@ -1,4 +1,8 @@
1
- import type { ChatComposerNode, ChatToolActionViewModel } from '@nextclaw/agent-chat-ui';
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
- closeChildSessionDetail: () => void;
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>
@@ -54,7 +54,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
54
54
  ) : (
55
55
  <main
56
56
  className={cn(
57
- 'flex-1 custom-scrollbar p-8',
57
+ 'flex-1 custom-scrollbar p-8 pb-24',
58
58
  lockPageScroll ? 'overflow-auto xl:overflow-hidden' : 'overflow-auto'
59
59
  )}
60
60
  >
@@ -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
+ }
@@ -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 ? truncateText(text, 120) : undefined;
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 truncateText(stringifyUnknown(parsed), 140);
96
+ return truncateInlineText(stringifyUnknown(parsed), 140);
86
97
  }
87
98
 
88
99
  function toToolName(value: unknown): string {
@@ -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
- chatChildSessionClosePanel: { zh: '关闭子会话侧栏', en: 'Close child session panel' },
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