@nextclaw/ui 0.12.8 → 0.12.10

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 (227) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/dist/assets/ChannelsList-M9FTK1Ak.js +8 -0
  3. package/dist/assets/DocBrowser-CH7-GxlL.js +1 -0
  4. package/dist/assets/{DocBrowser-BMxf9CIK.js → DocBrowser-DMfr0Oow.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-Ce28gRXt.js → DocBrowserContext-BXydqby-.js} +1 -1
  6. package/dist/assets/{LogoBadge-o92MOA2L.js → LogoBadge-hO7tY7hE.js} +1 -1
  7. package/dist/assets/ModelConfig-CNIgLf0e.js +1 -0
  8. package/dist/assets/{ProviderScopedModelInput-CmTIzgI7.js → ProviderScopedModelInput-B3HWP4oz.js} +1 -1
  9. package/dist/assets/ProvidersList-CHjMnRhX.js +1 -0
  10. package/dist/assets/RuntimeConfig-psp8nMSG.js +1 -0
  11. package/dist/assets/SearchConfig-CSoKip1f.js +1 -0
  12. package/dist/assets/{SecretsConfig-Ba1RPJaG.js → SecretsConfig-MEt6MjuD.js} +2 -2
  13. package/dist/assets/SessionsConfig-DifCiXwR.js +2 -0
  14. package/dist/assets/{app-query-client-DniXoIN5.js → app-query-client-9jNewezV.js} +1 -1
  15. package/dist/assets/{book-open-DocgeQtR.js → book-open-DzdUViDm.js} +1 -1
  16. package/dist/assets/chat-page-CLp0UV0Y.js +58 -0
  17. package/dist/assets/chat-session-display-DsYHx0RZ.js +1 -0
  18. package/dist/assets/{chunk-JZWAC4HX-BvKvh1R8.js → chunk-JZWAC4HX-C5dEc8hV.js} +1 -1
  19. package/dist/assets/{client-CVqPF5ie.js → client-C-8fH7-c.js} +1 -1
  20. package/dist/assets/{config-Bop2oB18.js → config-CBScxsdV.js} +1 -1
  21. package/dist/assets/config-split-page-BUout_Ak.js +1 -0
  22. package/dist/assets/{createLucideIcon-DVv8taGY.js → createLucideIcon-dy5ie7Ox.js} +1 -1
  23. package/dist/assets/desktop-update-config-2BS6BMkW.js +1 -0
  24. package/dist/assets/{dist-DmAlInRu.js → dist-BruyLa92.js} +1 -1
  25. package/dist/assets/{dist-Da5Gm_pO.js → dist-Cy7_j6hA.js} +1 -1
  26. package/dist/assets/download-BD0ETkB-.js +1 -0
  27. package/dist/assets/{external-link-DFjw3x1B.js → external-link-kZSAO8nT.js} +1 -1
  28. package/dist/assets/{hash-DJtaCejM.js → hash-BHJC2Ovu.js} +1 -1
  29. package/dist/assets/i18n-CpTZLchQ.js +1 -0
  30. package/dist/assets/index-mW8W2FUu.css +1 -0
  31. package/dist/assets/index-zDZfXoI4.js +6 -0
  32. package/dist/assets/{infiniteQueryBehavior-DHSEQ3OH.js → infiniteQueryBehavior-CyER9hv0.js} +1 -1
  33. package/dist/assets/loader-circle-Bc2gCU33.js +1 -0
  34. package/dist/assets/{logos-DEFUIR12.js → logos-B7gRObP8.js} +1 -1
  35. package/dist/assets/marketplace-page-3qVMnF3d.js +1 -0
  36. package/dist/assets/marketplace-page-BhFIeQzI.js +49 -0
  37. package/dist/assets/mcp-marketplace-page-DYfteJ1D.js +40 -0
  38. package/dist/assets/{page-layout-Da3i3r6G.js → page-layout-0UcO9H9Z.js} +1 -1
  39. package/dist/assets/play-CKDjSQFL.js +1 -0
  40. package/dist/assets/plus-CG0QrVY_.js +1 -0
  41. package/dist/assets/{refresh-ccw-D6HkNtfz.js → refresh-ccw-COVhNHtN.js} +1 -1
  42. package/dist/assets/{refresh-cw-DRcvRrnc.js → refresh-cw-Bcv40SXy.js} +1 -1
  43. package/dist/assets/remote-access-page-CWHG-sug.js +1 -0
  44. package/dist/assets/{rotate-cw-BmDKfXtH.js → rotate-cw-oHMKJMC8.js} +1 -1
  45. package/dist/assets/{save-DHGmi2e9.js → save-EqJPOF0G.js} +1 -1
  46. package/dist/assets/search-BCAlB8nz.js +1 -0
  47. package/dist/assets/security-config-Slh0Mayz.js +1 -0
  48. package/dist/assets/select-CVz0t7MF.js +41 -0
  49. package/dist/assets/setting-row-CbVHAuQt.js +1 -0
  50. package/dist/assets/skeleton-D5rdKvzy.js +1 -0
  51. package/dist/assets/{status-dot-DurKKSwA.js → status-dot-DpPtVzQT.js} +1 -1
  52. package/dist/assets/{switch-0rmPBRKI.js → switch-CM29eCAR.js} +1 -1
  53. package/dist/assets/{tabs-custom-5JLVL6v8.js → tabs-custom-YcZUWn3o.js} +1 -1
  54. package/dist/assets/tag-chip-DMXdnLcj.js +1 -0
  55. package/dist/assets/{trash-2-C6caKPoz.js → trash-2-mJT6oWa2.js} +1 -1
  56. package/dist/assets/{use-infinite-scroll-loader-dwnaa_qi.js → use-infinite-scroll-loader-DJ1L81Dz.js} +1 -1
  57. package/dist/assets/{useConfirmDialog-mMeWD_yo.js → useConfirmDialog-BsVuqu1x.js} +1 -1
  58. package/dist/assets/{useMutation-BmxxvCNf.js → useMutation-CNcz2fgt.js} +1 -1
  59. package/dist/assets/x-Czwxm82I.js +1 -0
  60. package/dist/index.html +95 -21
  61. package/dist/manifest.webmanifest +30 -0
  62. package/dist/offline.html +102 -0
  63. package/dist/pwa-192.png +0 -0
  64. package/dist/pwa-512.png +0 -0
  65. package/dist/runtime-icons/claude.ico +0 -0
  66. package/dist/runtime-icons/codex-openai.svg +6 -0
  67. package/dist/runtime-icons/hermes-agent.png +0 -0
  68. package/dist/sw.js +80 -0
  69. package/index.html +73 -1
  70. package/package.json +5 -5
  71. package/public/manifest.webmanifest +30 -0
  72. package/public/offline.html +102 -0
  73. package/public/pwa-192.png +0 -0
  74. package/public/pwa-512.png +0 -0
  75. package/public/runtime-icons/claude.ico +0 -0
  76. package/public/runtime-icons/codex-openai.svg +6 -0
  77. package/public/runtime-icons/hermes-agent.png +0 -0
  78. package/public/sw.js +80 -0
  79. package/src/account/components/account-panel.tsx +217 -97
  80. package/src/account/managers/account.manager.ts +3 -2
  81. package/src/api/chat-session-type.types.ts +7 -0
  82. package/src/api/runtime-control.types.ts +8 -0
  83. package/src/api/server-path.ts +27 -4
  84. package/src/api/types.ts +25 -10
  85. package/src/app.tsx +227 -54
  86. package/src/components/agents/agent-dialogs.tsx +499 -0
  87. package/src/components/agents/agents-page.test.tsx +238 -0
  88. package/src/components/agents/agents-page.tsx +435 -0
  89. package/src/components/chat/ChatSidebar.test.tsx +43 -1
  90. package/src/components/chat/ChatSidebar.tsx +35 -35
  91. package/src/components/chat/adapters/chat-message.summary-truncation.test.ts +66 -0
  92. package/src/components/chat/adapters/file-operation/card.ts +9 -0
  93. package/src/components/chat/adapters/file-operation/diff.ts +14 -0
  94. package/src/components/chat/{ChatConversationPanel.test.tsx → chat-conversation-panel.test.tsx} +127 -206
  95. package/src/components/chat/chat-conversation-panel.tsx +482 -0
  96. package/src/components/chat/chat-page-shell.tsx +19 -13
  97. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  98. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  99. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +178 -0
  100. package/src/components/chat/chat-session-workspace-file-preview.tsx +278 -0
  101. package/src/components/chat/chat-session-workspace-panel-nav.tsx +203 -0
  102. package/src/components/chat/chat-session-workspace-panel.tsx +318 -0
  103. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  104. package/src/components/chat/chat-sidebar-session-item.tsx +32 -2
  105. package/src/components/chat/containers/chat-message-list.container.test.tsx +49 -0
  106. package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
  107. package/src/components/chat/managers/chat-session-list.manager.test.ts +12 -0
  108. package/src/components/chat/managers/chat-session-list.manager.ts +7 -0
  109. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  110. package/src/components/chat/ncp/ncp-chat-page.tsx +9 -7
  111. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +179 -41
  112. package/src/components/chat/ncp/ncp-session-adapter.test.ts +36 -1
  113. package/src/components/chat/ncp/ncp-session-adapter.ts +20 -0
  114. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +62 -13
  115. package/src/components/chat/ncp/tests/ncp-chat-thread.manager.test.ts +189 -0
  116. package/src/components/chat/presenter/chat-presenter-context.tsx +13 -2
  117. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +26 -0
  118. package/src/components/chat/session-header/chat-session-header-actions.tsx +19 -1
  119. package/src/components/chat/stores/chat-input.store.ts +2 -1
  120. package/src/components/chat/stores/chat-thread.store.ts +27 -1
  121. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  122. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  123. package/src/components/common/BrandHeader.tsx +3 -1
  124. package/src/components/common/session-context-icon.tsx +15 -2
  125. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  126. package/src/components/config/ChannelForm.test.tsx +89 -3
  127. package/src/components/config/ChannelForm.tsx +157 -188
  128. package/src/components/config/ChannelsList.test.tsx +163 -119
  129. package/src/components/config/ChannelsList.tsx +90 -101
  130. package/src/components/config/ProviderForm.tsx +108 -146
  131. package/src/components/config/ProvidersList.tsx +100 -123
  132. package/src/components/config/RuntimeConfig.tsx +141 -2
  133. package/src/components/config/SearchConfig.tsx +423 -393
  134. package/src/components/config/channel-form-fields-section.tsx +70 -37
  135. package/src/components/config/config-split-page.tsx +109 -0
  136. package/src/components/config/provider-enabled-field.tsx +17 -10
  137. package/src/components/config/runtime-control-card.test.tsx +56 -0
  138. package/src/components/config/runtime-control-card.tsx +25 -0
  139. package/src/components/config/runtime-presence-card.tsx +93 -79
  140. package/src/components/layout/AppLayout.tsx +25 -37
  141. package/src/components/layout/app-layout.test.tsx +46 -14
  142. package/src/components/layout/runtime-status-entry.test.tsx +157 -0
  143. package/src/components/layout/runtime-status-entry.tsx +143 -0
  144. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  145. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  146. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  147. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  148. package/src/components/marketplace/marketplace-page.tsx +596 -0
  149. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  150. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  151. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  152. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  153. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  154. package/src/components/providers/ThemeProvider.tsx +5 -0
  155. package/src/components/remote/remote-access-page.test.tsx +105 -0
  156. package/src/components/remote/remote-access-page.tsx +248 -0
  157. package/src/components/ui/notice-card.tsx +129 -0
  158. package/src/components/ui/setting-row.tsx +51 -0
  159. package/src/components/ui/tag-chip.tsx +39 -0
  160. package/src/components/ui/textarea.tsx +19 -0
  161. package/src/hooks/server-path/use-server-path-read.ts +20 -0
  162. package/src/hooks/useConfig.ts +2 -1
  163. package/src/index.css +24 -0
  164. package/src/lib/app-resource-uri.test.ts +20 -0
  165. package/src/lib/app-resource-uri.ts +29 -0
  166. package/src/lib/chat-message.ts +14 -3
  167. package/src/lib/i18n.chat.ts +12 -1
  168. package/src/lib/i18n.pwa.ts +62 -0
  169. package/src/lib/i18n.remote.ts +1 -1
  170. package/src/lib/i18n.runtime-control.ts +31 -0
  171. package/src/lib/i18n.ts +7 -10
  172. package/src/lib/session-context.utils.test.ts +71 -0
  173. package/src/lib/session-context.utils.ts +28 -3
  174. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  175. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  176. package/src/pwa/components/pwa-install-entry.test.tsx +110 -0
  177. package/src/pwa/components/pwa-install-entry.tsx +205 -0
  178. package/src/pwa/managers/pwa-install.manager.test.ts +160 -0
  179. package/src/pwa/managers/pwa-install.manager.ts +232 -0
  180. package/src/pwa/managers/pwa-runtime.manager.ts +196 -0
  181. package/src/pwa/managers/pwa-shell-theme.manager.test.ts +30 -0
  182. package/src/pwa/managers/pwa-shell-theme.manager.ts +46 -0
  183. package/src/pwa/pwa-install-banner.storage.ts +55 -0
  184. package/src/pwa/pwa.types.ts +22 -0
  185. package/src/pwa/register-pwa.ts +14 -0
  186. package/src/pwa/stores/pwa.store.ts +17 -0
  187. package/src/vite-env.d.ts +9 -0
  188. package/dist/assets/ChannelsList-KIQIxluX.js +0 -8
  189. package/dist/assets/DocBrowser-CyDgAtO9.js +0 -1
  190. package/dist/assets/MarketplacePage-BySqkYDh.js +0 -49
  191. package/dist/assets/MarketplacePage-C0olZaek.js +0 -1
  192. package/dist/assets/McpMarketplacePage-DqKaiXO9.js +0 -40
  193. package/dist/assets/ModelConfig-IrmzoslW.js +0 -1
  194. package/dist/assets/ProvidersList-8_Kalfwl.js +0 -1
  195. package/dist/assets/RemoteAccessPage-CyQlSjPf.js +0 -1
  196. package/dist/assets/RuntimeConfig-Bk0uYBhf.js +0 -1
  197. package/dist/assets/SearchConfig-DNBR-UbE.js +0 -1
  198. package/dist/assets/SessionsConfig-Doqp5ghH.js +0 -2
  199. package/dist/assets/chat-page-Bph8M5zo.js +0 -58
  200. package/dist/assets/chat-session-display-CoN3Wmn-.js +0 -1
  201. package/dist/assets/config-layout-DmlGaay2.js +0 -1
  202. package/dist/assets/desktop-update-config-1KBrqLBC.js +0 -1
  203. package/dist/assets/i18n-CwHZ-9vt.js +0 -1
  204. package/dist/assets/index-DafCdM4F.css +0 -1
  205. package/dist/assets/index-DdksE6U3.js +0 -6
  206. package/dist/assets/loader-circle-PsSP0H9n.js +0 -1
  207. package/dist/assets/play-DBQbBxTA.js +0 -1
  208. package/dist/assets/plus-DUOVbsyQ.js +0 -1
  209. package/dist/assets/popover-C_mWOFzI.js +0 -1
  210. package/dist/assets/search-MChQRYR1.js +0 -1
  211. package/dist/assets/security-config-CbXfPZzr.js +0 -1
  212. package/dist/assets/select-Caud8QvU.js +0 -41
  213. package/dist/assets/skeleton-B-4vRq_Z.js +0 -1
  214. package/dist/assets/x-DuMhMATD.js +0 -1
  215. package/src/components/agents/AgentDialogs.tsx +0 -400
  216. package/src/components/agents/AgentsPage.test.tsx +0 -217
  217. package/src/components/agents/AgentsPage.tsx +0 -352
  218. package/src/components/chat/ChatConversationPanel.tsx +0 -256
  219. package/src/components/chat/chat-child-session-panel.tsx +0 -270
  220. package/src/components/config/config-layout.ts +0 -10
  221. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  222. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  223. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  224. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  225. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  226. package/src/components/remote/RemoteAccessPage.tsx +0 -144
  227. /package/dist/assets/{config-hints-BZoDjXye.js → config-hints-BhTmc9P1.js} +0 -0
@@ -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
+ };
@@ -5,7 +5,7 @@ export const REMOTE_LABELS: Record<string, { zh: string; en: string }> = {
5
5
  en: 'Make this device appear in your NextClaw Platform device list and open it from the web.'
6
6
  },
7
7
  remoteOpenWeb: { zh: '前往 NextClaw Web', en: 'Open NextClaw Web' },
8
- remoteOpenDeviceList: { zh: '查看我的设备', en: 'View My Devices' },
8
+ remoteOpenDeviceList: { zh: '前往 NextClaw Web', en: 'Open NextClaw Web' },
9
9
  remoteOpenWebHint: {
10
10
  zh: '开启后,这台设备会出现在 NextClaw Web 中,你可以在那里点击打开并继续使用。',
11
11
  en: 'Once enabled, this device appears in NextClaw Web, where you can open it and keep working.'
@@ -75,6 +75,37 @@ export const RUNTIME_CONTROL_LABELS: Record<string, { zh: string; en: string }>
75
75
  runtimeControlRestartService: { zh: '重启服务', en: 'Restart Service' },
76
76
  runtimeControlStopService: { zh: '停止服务', en: 'Stop Service' },
77
77
  runtimeControlRestartApp: { zh: '重启应用', en: 'Restart App' },
78
+ runtimeControlPendingRestartTitle: { zh: '待重启', en: 'Pending Restart' },
79
+ runtimeControlPendingRestartDescription: {
80
+ zh: '这次改动已经保存,但系统不会自动重启。请在你方便的时候手动重启,重启完成后该提示会自动清空。',
81
+ en: 'These changes are saved, but the system will not restart automatically. Restart manually when you are ready, and this notice clears after the restart finishes.'
82
+ },
83
+ runtimeControlPendingRestartPaths: { zh: '待生效项', en: 'Changes Waiting For Restart' },
84
+ runtimeStatusLoadingTitle: { zh: '读取状态中', en: 'Loading status' },
85
+ runtimeStatusLoadingDescription: {
86
+ zh: '正在读取当前系统状态。',
87
+ en: 'Loading the current system status.'
88
+ },
89
+ runtimeStatusHealthyTitle: { zh: '系统正常', en: 'System healthy' },
90
+ runtimeStatusHealthyDescription: {
91
+ zh: '当前没有需要你立即处理的系统动作。',
92
+ en: 'There is no system action that needs your attention right now.'
93
+ },
94
+ runtimeStatusPendingRestartTitle: { zh: '待重启', en: 'Restart required' },
95
+ runtimeStatusPendingRestartDescription: {
96
+ zh: '这些改动已经保存,但不会自动重启。你可以在这里查看原因,并在方便的时候手动重启。',
97
+ en: 'These changes are saved, but the system will not restart automatically. Review the reason here and restart when you are ready.'
98
+ },
99
+ runtimeStatusPendingRestartReasonItem: {
100
+ zh: '{path} 改动将在重启后生效。',
101
+ en: 'Changes in {path} will apply after restart.'
102
+ },
103
+ runtimeStatusActionHint: {
104
+ zh: '准备好时再执行',
105
+ en: 'Run when you are ready'
106
+ },
107
+ runtimeStatusRestartAction: { zh: '立即重启', en: 'Restart now' },
108
+ runtimeStatusRestartingAction: { zh: '重启中...', en: 'Restarting...' },
78
109
  runtimeControlStartingServiceHelp: {
79
110
  zh: '正在启动 NextClaw 服务,页面可能会在服务恢复后重新连接。',
80
111
  en: 'Starting the NextClaw service. The page may reconnect after the service becomes available.'
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 : '-';
@@ -497,27 +497,23 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
497
497
 
498
498
  // Remote & Status
499
499
  ...REMOTE_LABELS,
500
-
501
- // Action labels
502
500
  actionConfigure: { zh: '配置', en: 'Configure' },
503
501
  actionAddProvider: { zh: '添加提供商', en: 'Add Provider' },
504
502
  actionEnable: { zh: '启用', en: 'Enable' },
505
-
506
- // Messages
507
503
  configSaved: { zh: '配置已保存', en: 'Configuration saved' },
504
+ configSavedApplying: { zh: '配置已保存,正在应用', en: 'Configuration saved, applying changes' },
508
505
  configSavedApplied: { zh: '配置已保存并已应用', en: 'Configuration saved and applied' },
509
506
  configSaveFailed: { zh: '保存配置失败', en: 'Failed to save configuration' },
510
507
  configReloaded: { zh: '配置已重载', en: 'Configuration reloaded' },
511
508
  configReloadFailed: { zh: '重载配置失败', en: 'Failed to reload configuration' },
512
- feishuVerifySuccess: {
513
- zh: '验证成功,请到飞书开放平台完成事件订阅与发布后再开始使用。',
514
- en: 'Verified. Please finish Feishu event subscription and app publishing before using.'
515
- },
509
+ channelConfigApplying: { zh: '渠道配置正在应用', en: 'Channel configuration is applying' },
510
+ channelConfigApplied: { zh: '渠道配置已应用', en: 'Channel configuration applied' },
511
+ channelConfigApplyFailed: { zh: '渠道配置应用失败', en: 'Failed to apply channel configuration' },
512
+ feishuVerifySuccess: { zh: '验证成功,请到飞书开放平台完成事件订阅与发布后再开始使用。', en: 'Verified. Please finish Feishu event subscription and app publishing before using.' },
516
513
  feishuVerifyFailed: { zh: '验证失败', en: 'Verification failed' },
517
514
  enterTag: { zh: '输入后按回车...', en: 'Type and press Enter...' },
518
515
  headerName: { zh: 'Header 名称', en: 'Header Name' },
519
516
  headerValue: { zh: 'Header 值', en: 'Header Value' },
520
-
521
517
  // Doc Browser
522
518
  docBrowserTitle: { zh: '内嵌浏览器', en: 'Embedded Browser' },
523
519
  docBrowserSearchPlaceholder: { zh: '搜索,也可以输入文档地址直接打开', en: 'Search, or enter a doc URL to open' },
@@ -531,6 +527,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
531
527
  docBrowserCloseTab: { zh: '关闭标签', en: 'Close Tab' },
532
528
  docBrowserTabUntitled: { zh: '未命名', en: 'Untitled' },
533
529
  ...PATH_PICKER_LABELS,
530
+ ...PWA_LABELS,
534
531
  ...CHANNEL_AUTH_LABELS,
535
532
  };
536
533
 
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { resolveSessionContextView } from "@/lib/session-context.utils";
3
+
4
+ vi.mock("@/lib/logos", () => ({
5
+ getChannelLogo: vi.fn(() => null),
6
+ }));
7
+
8
+ vi.mock("@/lib/i18n", () => ({
9
+ t: (key: string) => key,
10
+ }));
11
+
12
+ describe("resolveSessionContextView", () => {
13
+ it("prefers a declared runtime icon before falling back to a text label", () => {
14
+ const view = resolveSessionContextView(
15
+ {
16
+ key: "session-1",
17
+ createdAt: "2026-04-18T00:00:00.000Z",
18
+ updatedAt: "2026-04-18T00:00:00.000Z",
19
+ sessionType: "hermes",
20
+ sessionTypeMutable: true,
21
+ messageCount: 1,
22
+ },
23
+ [
24
+ {
25
+ value: "hermes",
26
+ label: "Hermes",
27
+ icon: {
28
+ kind: "image",
29
+ src: "app://runtime-icons/hermes-agent.png",
30
+ alt: "Hermes",
31
+ },
32
+ },
33
+ ],
34
+ );
35
+
36
+ expect(view).toEqual({
37
+ icon: {
38
+ kind: "runtime-image",
39
+ src: "app://runtime-icons/hermes-agent.png",
40
+ alt: "Hermes",
41
+ name: "Hermes",
42
+ },
43
+ label: null,
44
+ });
45
+ });
46
+
47
+ it("falls back to the resolved runtime label when no runtime icon is available", () => {
48
+ const view = resolveSessionContextView(
49
+ {
50
+ key: "session-2",
51
+ createdAt: "2026-04-18T00:00:00.000Z",
52
+ updatedAt: "2026-04-18T00:00:00.000Z",
53
+ sessionType: "custom-runtime",
54
+ sessionTypeMutable: true,
55
+ messageCount: 2,
56
+ },
57
+ [
58
+ {
59
+ value: "custom-runtime",
60
+ label: "Custom Runtime",
61
+ icon: null,
62
+ },
63
+ ],
64
+ );
65
+
66
+ expect(view).toEqual({
67
+ icon: null,
68
+ label: "Custom Runtime",
69
+ });
70
+ });
71
+ });
@@ -1,4 +1,4 @@
1
- import type { SessionEntryView } from '@/api/types';
1
+ import type { SessionEntryView, SessionTypeIconView } from '@/api/types';
2
2
  import { t } from '@/lib/i18n';
3
3
  import { getChannelLogo } from '@/lib/logos';
4
4
 
@@ -6,6 +6,7 @@ type SessionContextSymbolIcon = 'heartbeat' | 'cron';
6
6
 
7
7
  export type SessionContextIcon =
8
8
  | { kind: 'channel-logo'; channel: string }
9
+ | { kind: 'runtime-image'; src: string; alt?: string | null; name?: string | null }
9
10
  | { kind: 'symbol'; icon: SessionContextSymbolIcon };
10
11
 
11
12
  export type SessionContextView = {
@@ -55,6 +56,17 @@ function resolveSessionTypeLabel(
55
56
  return toTitleCaseByDelimiter(normalized);
56
57
  }
57
58
 
59
+ function resolveSessionTypeOption(
60
+ sessionType: string,
61
+ options: Array<{ value: string; label: string; icon?: SessionTypeIconView | null }>
62
+ ): { value: string; label: string; icon?: SessionTypeIconView | null } | null {
63
+ const normalized = normalize(sessionType);
64
+ if (!normalized || normalized === 'native') {
65
+ return null;
66
+ }
67
+ return options.find((option) => normalize(option.value) === normalized) ?? null;
68
+ }
69
+
58
70
  function resolveChannelLogoName(channel: string): string | null {
59
71
  const normalized = normalize(channel);
60
72
  if (!normalized) {
@@ -66,7 +78,7 @@ function resolveChannelLogoName(channel: string): string | null {
66
78
 
67
79
  export function resolveSessionContextView(
68
80
  session: SessionEntryView,
69
- options: Array<{ value: string; label: string }>
81
+ options: Array<{ value: string; label: string; icon?: SessionTypeIconView | null }>
70
82
  ): SessionContextView {
71
83
  const logoChannel = resolveChannelLogoName(session.channel ?? '');
72
84
  if (logoChannel) {
@@ -83,6 +95,18 @@ export function resolveSessionContextView(
83
95
  }
84
96
 
85
97
  const sessionType = normalize(session.sessionType);
98
+ const matchedSessionTypeOption = resolveSessionTypeOption(sessionType, options);
99
+ if (matchedSessionTypeOption?.icon?.src?.trim()) {
100
+ return {
101
+ icon: {
102
+ kind: 'runtime-image',
103
+ src: matchedSessionTypeOption.icon.src,
104
+ alt: matchedSessionTypeOption.icon.alt ?? null,
105
+ name: matchedSessionTypeOption.label
106
+ },
107
+ label: null,
108
+ };
109
+ }
86
110
  const labelKey = SESSION_TYPE_LABEL_REGISTRY[sessionType];
87
111
  if (labelKey) {
88
112
  return { icon: null, label: t(labelKey) };
@@ -90,6 +114,7 @@ export function resolveSessionContextView(
90
114
 
91
115
  return {
92
116
  icon: null,
93
- label: resolveSessionTypeLabel(session.sessionType, options),
117
+ label:
118
+ matchedSessionTypeOption?.label?.trim() || resolveSessionTypeLabel(session.sessionType, options),
94
119
  };
95
120
  }
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildWorkspaceFileBreadcrumb } from "@/lib/session-project/workspace-file-breadcrumb";
3
+
4
+ describe("buildWorkspaceFileBreadcrumb", () => {
5
+ it("builds project-relative breadcrumbs for files inside the active workspace", () => {
6
+ const breadcrumb = buildWorkspaceFileBreadcrumb({
7
+ path: "/Users/demo/project-alpha/src/chat/example.tsx",
8
+ sessionProjectRoot: "/Users/demo/project-alpha",
9
+ truncated: false,
10
+ });
11
+
12
+ expect(breadcrumb.segments).toEqual([
13
+ {
14
+ key: "workspace:project-alpha",
15
+ label: "project-alpha",
16
+ kind: "workspace",
17
+ isCurrent: false,
18
+ },
19
+ {
20
+ key: "0:src",
21
+ label: "src",
22
+ kind: "directory",
23
+ isCurrent: false,
24
+ },
25
+ {
26
+ key: "1:chat",
27
+ label: "chat",
28
+ kind: "directory",
29
+ isCurrent: false,
30
+ },
31
+ {
32
+ key: "2:example.tsx",
33
+ label: "example.tsx",
34
+ kind: "file",
35
+ isCurrent: true,
36
+ },
37
+ ]);
38
+ });
39
+
40
+ it("keeps absolute path breadcrumbs when the file sits outside the workspace root", () => {
41
+ const breadcrumb = buildWorkspaceFileBreadcrumb({
42
+ path: "/tmp/example.ts",
43
+ sessionProjectRoot: "/Users/demo/project-alpha",
44
+ truncated: false,
45
+ });
46
+
47
+ expect(breadcrumb.segments).toEqual([
48
+ {
49
+ key: "root:/",
50
+ label: "/",
51
+ kind: "root",
52
+ isCurrent: false,
53
+ },
54
+ {
55
+ key: "0:tmp",
56
+ label: "tmp",
57
+ kind: "directory",
58
+ isCurrent: false,
59
+ },
60
+ {
61
+ key: "1:example.ts",
62
+ label: "example.ts",
63
+ kind: "file",
64
+ isCurrent: true,
65
+ },
66
+ ]);
67
+ });
68
+
69
+ it("attaches line metadata for the workspace breadcrumb bar", () => {
70
+ const breadcrumb = buildWorkspaceFileBreadcrumb({
71
+ path: "README.md",
72
+ sessionProjectRoot: "/Users/demo/project-alpha",
73
+ line: 12,
74
+ column: 4,
75
+ truncated: true,
76
+ });
77
+
78
+ expect(breadcrumb.locationLabel).toBe("L12:4");
79
+ expect(breadcrumb.truncated).toBe(true);
80
+ expect(breadcrumb.segments[0]?.label).toBe("project-alpha");
81
+ expect(breadcrumb.segments[1]?.label).toBe("README.md");
82
+ });
83
+ });
@@ -0,0 +1,188 @@
1
+ import { getSessionProjectName } from "@/lib/session-project/session-project.utils";
2
+
3
+ export type WorkspaceFileBreadcrumbSegmentViewModel = {
4
+ key: string;
5
+ label: string;
6
+ kind: "workspace" | "root" | "directory" | "file";
7
+ isCurrent: boolean;
8
+ };
9
+
10
+ export type WorkspaceFileBreadcrumbViewModel = {
11
+ fullPath: string;
12
+ locationLabel: string | null;
13
+ truncated: boolean;
14
+ segments: WorkspaceFileBreadcrumbSegmentViewModel[];
15
+ };
16
+
17
+ function trimPathSeparators(value: string): string {
18
+ if (/^[A-Za-z]:[\\/]?$/.test(value) || value === "/") {
19
+ return value;
20
+ }
21
+ return value.replace(/[\\/]+$/, "");
22
+ }
23
+
24
+ function normalizeComparablePath(value: string): string {
25
+ const trimmed = trimPathSeparators(value.trim().replace(/\\/g, "/"));
26
+ return /^[A-Za-z]:/.test(trimmed)
27
+ ? `${trimmed.slice(0, 1).toLowerCase()}${trimmed.slice(1)}`
28
+ : trimmed;
29
+ }
30
+
31
+ function readDisplaySegments(value: string): {
32
+ prefix: string | null;
33
+ segments: string[];
34
+ } {
35
+ const normalized = value.trim().replace(/\\/g, "/");
36
+
37
+ if (!normalized) {
38
+ return { prefix: null, segments: [] };
39
+ }
40
+
41
+ if (/^[A-Za-z]:\//.test(normalized)) {
42
+ return {
43
+ prefix: normalized.slice(0, 2),
44
+ segments: normalized.slice(3).split("/").filter(Boolean),
45
+ };
46
+ }
47
+
48
+ if (normalized.startsWith("/")) {
49
+ return {
50
+ prefix: "/",
51
+ segments: normalized.slice(1).split("/").filter(Boolean),
52
+ };
53
+ }
54
+
55
+ return {
56
+ prefix: null,
57
+ segments: normalized.split("/").filter(Boolean),
58
+ };
59
+ }
60
+
61
+ function readRelativeSegments(params: {
62
+ path: string;
63
+ sessionProjectRoot: string;
64
+ }): string[] | null {
65
+ const normalizedPath = normalizeComparablePath(params.path);
66
+ const normalizedRoot = normalizeComparablePath(params.sessionProjectRoot);
67
+
68
+ if (!normalizedPath || !normalizedRoot) {
69
+ return null;
70
+ }
71
+
72
+ if (
73
+ !normalizedPath.startsWith("/") &&
74
+ !/^[A-Za-z]:\//.test(normalizedPath)
75
+ ) {
76
+ return normalizedPath.split("/").filter(Boolean);
77
+ }
78
+
79
+ if (normalizedPath === normalizedRoot) {
80
+ return [];
81
+ }
82
+
83
+ const rootPrefix = normalizedRoot.endsWith("/")
84
+ ? normalizedRoot
85
+ : `${normalizedRoot}/`;
86
+
87
+ if (!normalizedPath.startsWith(rootPrefix)) {
88
+ return null;
89
+ }
90
+
91
+ return normalizedPath.slice(rootPrefix.length).split("/").filter(Boolean);
92
+ }
93
+
94
+ function buildSegmentsFromLabels(params: {
95
+ labels: string[];
96
+ leading?: WorkspaceFileBreadcrumbSegmentViewModel | null;
97
+ }): WorkspaceFileBreadcrumbSegmentViewModel[] {
98
+ const { labels, leading = null } = params;
99
+ const items = labels.map<WorkspaceFileBreadcrumbSegmentViewModel>(
100
+ (label, index) => ({
101
+ key: `${index}:${label}`,
102
+ label,
103
+ kind: index === labels.length - 1 ? "file" : "directory",
104
+ isCurrent: index === labels.length - 1,
105
+ }),
106
+ );
107
+
108
+ return leading ? [leading, ...items] : items;
109
+ }
110
+
111
+ function buildLocationLabel(params: {
112
+ line?: number | null;
113
+ column?: number | null;
114
+ }): string | null {
115
+ const { column, line } = params;
116
+
117
+ if (typeof line !== "number") {
118
+ return null;
119
+ }
120
+
121
+ return `L${line}${typeof column === "number" ? `:${column}` : ""}`;
122
+ }
123
+
124
+ export function buildWorkspaceFileBreadcrumb(params: {
125
+ path: string;
126
+ sessionProjectRoot: string | null;
127
+ line?: number | null;
128
+ column?: number | null;
129
+ truncated: boolean;
130
+ }): WorkspaceFileBreadcrumbViewModel {
131
+ const { column, line, path, sessionProjectRoot, truncated } = params;
132
+ const fullPath = path.trim();
133
+ const relativeSegments =
134
+ sessionProjectRoot?.trim() && fullPath
135
+ ? readRelativeSegments({
136
+ path: fullPath,
137
+ sessionProjectRoot,
138
+ })
139
+ : null;
140
+
141
+ let segments: WorkspaceFileBreadcrumbSegmentViewModel[];
142
+
143
+ if (sessionProjectRoot?.trim() && relativeSegments) {
144
+ const workspaceLabel =
145
+ getSessionProjectName(sessionProjectRoot) ?? sessionProjectRoot.trim();
146
+
147
+ segments = buildSegmentsFromLabels({
148
+ labels: relativeSegments,
149
+ leading: {
150
+ key: `workspace:${workspaceLabel}`,
151
+ label: workspaceLabel,
152
+ kind: "workspace",
153
+ isCurrent: relativeSegments.length === 0,
154
+ },
155
+ });
156
+ } else {
157
+ const { prefix, segments: labels } = readDisplaySegments(fullPath);
158
+ segments = buildSegmentsFromLabels({
159
+ labels,
160
+ leading: prefix
161
+ ? {
162
+ key: `root:${prefix}`,
163
+ label: prefix,
164
+ kind: "root",
165
+ isCurrent: labels.length === 0,
166
+ }
167
+ : null,
168
+ });
169
+ }
170
+
171
+ if (segments.length === 0) {
172
+ segments = [
173
+ {
174
+ key: "file:unknown",
175
+ label: fullPath || "file",
176
+ kind: "file",
177
+ isCurrent: true,
178
+ },
179
+ ];
180
+ }
181
+
182
+ return {
183
+ fullPath,
184
+ locationLabel: buildLocationLabel({ line, column }),
185
+ truncated,
186
+ segments,
187
+ };
188
+ }