@nextclaw/ui 0.12.9 → 0.12.11

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 (245) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/assets/ChannelsList-SQ7Oxotv.js +8 -0
  3. package/dist/assets/DocBrowser-BCO2k6XD.js +1 -0
  4. package/dist/assets/{DocBrowser-6ReNjvzF.js → DocBrowser-rDOjI3ga.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-B6SpA7Qs.js → DocBrowserContext-BUq3Wo8O.js} +1 -1
  6. package/dist/assets/{LogoBadge-ByNLYg65.js → LogoBadge-DP8Ye7wJ.js} +1 -1
  7. package/dist/assets/ModelConfig-C77Ae9ru.js +1 -0
  8. package/dist/assets/ProviderScopedModelInput-CEnK61uo.js +1 -0
  9. package/dist/assets/ProvidersList-BCupBayq.js +1 -0
  10. package/dist/assets/RuntimeConfig-Ad-CAcmy.js +1 -0
  11. package/dist/assets/SearchConfig-BfCz4wJ4.js +1 -0
  12. package/dist/assets/SecretsConfig-DjmBIhyy.js +3 -0
  13. package/dist/assets/{SessionsConfig-ChHQ7M5c.js → SessionsConfig-CvjxU40H.js} +2 -2
  14. package/dist/assets/{book-open-BdcxxoQu.js → book-open-BE8M56IM.js} +1 -1
  15. package/dist/assets/chat-page-JKC6ln-y.js +58 -0
  16. package/dist/assets/chat-session-display-YcRMrAMa.js +1 -0
  17. package/dist/assets/{chunk-JZWAC4HX-DK5HPmIK.js → chunk-JZWAC4HX-erTUn3b8.js} +1 -1
  18. package/dist/assets/client-CszWMVKi.js +7 -0
  19. package/dist/assets/config-split-page-BAGSzUR3.js +1 -0
  20. package/dist/assets/{createLucideIcon-BSeTgkZW.js → createLucideIcon-CCiTGX8L.js} +1 -1
  21. package/dist/assets/desktop-DfkLlkG2.js +1 -0
  22. package/dist/assets/desktop-update-config-BXeGlqHD.js +1 -0
  23. package/dist/assets/dialog-BghZFPch.js +5 -0
  24. package/dist/assets/{dist-6TrrnPCR.js → dist-Dd9cr-kz.js} +1 -1
  25. package/dist/assets/dist-ZwoAXs46.js +9 -0
  26. package/dist/assets/{download-BhDxnyvU.js → download-D7LOizcW.js} +1 -1
  27. package/dist/assets/es2015-CEAreese.js +41 -0
  28. package/dist/assets/{external-link-BgErLCNT.js → external-link-qsnCMhw1.js} +1 -1
  29. package/dist/assets/{hash-Bl7dr_UG.js → hash-0zjWsNl-.js} +1 -1
  30. package/dist/assets/{i18n-eDHeDY0n.js → i18n-DvzXOGQX.js} +1 -1
  31. package/dist/assets/index-DvVTC9FF.css +1 -0
  32. package/dist/assets/index-lr6rQUSd.js +2 -0
  33. package/dist/assets/key-round-BLe9D8ND.js +1 -0
  34. package/dist/assets/loader-circle-wj7kARHv.js +1 -0
  35. package/dist/assets/{logos-x89HbrZ4.js → logos-_v5b2SdG.js} +1 -1
  36. package/dist/assets/marketplace-page-CAAk1Khc.js +1 -0
  37. package/dist/assets/marketplace-page-CfCiq90S.js +49 -0
  38. package/dist/assets/mcp-marketplace-page-D0Pp9Hs-.js +40 -0
  39. package/dist/assets/play-o6NmwGTi.js +1 -0
  40. package/dist/assets/plus-I9pBS4Fl.js +1 -0
  41. package/dist/assets/{refresh-cw-C47QSEwg.js → refresh-cw-MNqgR3LZ.js} +1 -1
  42. package/dist/assets/remote-C9fXm4V5.js +1 -0
  43. package/dist/assets/{save-3S6-H3Xw.js → save-D4bObrmH.js} +1 -1
  44. package/dist/assets/search-DxmL3IWE.js +1 -0
  45. package/dist/assets/security-config-BUm6FFfl.js +1 -0
  46. package/dist/assets/select-BILPf7zs.js +1 -0
  47. package/dist/assets/setting-row-BATDgg4r.js +1 -0
  48. package/dist/assets/skeleton-COKMAnJy.js +1 -0
  49. package/dist/assets/{switch-BsLtHOH-.js → switch-CBOzecWS.js} +1 -1
  50. package/dist/assets/{tabs-custom-D3HYMt6k.js → tabs-custom-Bx3cNhD-.js} +1 -1
  51. package/dist/assets/tag-chip-zUaDE2-H.js +1 -0
  52. package/dist/assets/{trash-2-G48scll7.js → trash-2-CQUgYyRn.js} +1 -1
  53. package/dist/assets/use-infinite-scroll-loader-B5V2Klve.js +1 -0
  54. package/dist/assets/useConfirmDialog-patAnl1g.js +1 -0
  55. package/dist/assets/{useMutation-CBWjE2uj.js → useMutation-__AYv-Pz.js} +1 -1
  56. package/dist/assets/x-BHUGQIUv.js +1 -0
  57. package/dist/index.html +22 -22
  58. package/dist/runtime-icons/claude.ico +0 -0
  59. package/dist/runtime-icons/codex-openai.svg +6 -0
  60. package/dist/runtime-icons/hermes-agent.png +0 -0
  61. package/module-structure.config.json +7 -0
  62. package/package.json +6 -6
  63. package/public/runtime-icons/claude.ico +0 -0
  64. package/public/runtime-icons/codex-openai.svg +6 -0
  65. package/public/runtime-icons/hermes-agent.png +0 -0
  66. package/src/api/chat-session-type.types.ts +7 -0
  67. package/src/api/config.ts +10 -0
  68. package/src/api/raw-client.test.ts +1 -1
  69. package/src/api/{raw-client.ts → raw-client.utils.ts} +2 -0
  70. package/src/api/runtime-control.types.ts +8 -0
  71. package/src/api/types.ts +48 -0
  72. package/src/app/components/app-manager-provider.tsx +20 -0
  73. package/src/app/managers/app.manager.ts +12 -0
  74. package/src/app.tsx +223 -59
  75. package/src/components/agents/agent-dialogs.tsx +499 -0
  76. package/src/components/agents/agents-page.test.tsx +238 -0
  77. package/src/components/agents/agents-page.tsx +435 -0
  78. package/src/components/chat/chat-conversation-panel.test.tsx +30 -0
  79. package/src/components/chat/chat-conversation-panel.tsx +83 -13
  80. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.test.ts +92 -0
  81. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.ts +45 -0
  82. package/src/components/chat/chat-page-shell.tsx +19 -13
  83. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  84. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  85. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +87 -0
  86. package/src/components/chat/chat-session-workspace-file-preview.tsx +14 -43
  87. package/src/components/chat/chat-session-workspace-panel-nav.tsx +8 -2
  88. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  89. package/src/components/chat/containers/chat-input-bar.container.tsx +24 -12
  90. package/src/components/chat/{ChatSidebar.test.tsx → containers/chat-sidebar.test.tsx} +5 -4
  91. package/src/components/chat/{ChatSidebar.tsx → containers/chat-sidebar.tsx} +24 -72
  92. package/src/components/chat/hooks/use-chat-sidebar-session-label-editor.ts +49 -0
  93. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  94. package/src/components/chat/ncp/ncp-app-client-fetch.ts +3 -0
  95. package/src/components/chat/ncp/ncp-chat-input.manager.ts +13 -5
  96. package/src/components/chat/ncp/ncp-chat-page.tsx +23 -2
  97. package/src/components/chat/ncp/ncp-session-adapter.test.ts +1 -0
  98. package/src/components/chat/ncp/ncp-session-adapter.ts +3 -0
  99. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +10 -4
  100. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.test.tsx +48 -4
  101. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.ts +43 -5
  102. package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +51 -1
  103. package/src/components/chat/stores/chat-input.store.ts +2 -1
  104. package/src/components/chat/stores/chat-thread.store.ts +3 -1
  105. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  106. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  107. package/src/components/common/BrandHeader.tsx +3 -1
  108. package/src/components/common/session-context-icon.tsx +15 -2
  109. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  110. package/src/components/config/ChannelForm.test.tsx +89 -3
  111. package/src/components/config/ChannelForm.tsx +157 -188
  112. package/src/components/config/ChannelsList.test.tsx +163 -119
  113. package/src/components/config/ChannelsList.tsx +90 -101
  114. package/src/components/config/ProviderForm.tsx +108 -146
  115. package/src/components/config/ProvidersList.tsx +100 -123
  116. package/src/components/config/SearchConfig.tsx +423 -393
  117. package/src/components/config/channel-form-fields-section.tsx +70 -37
  118. package/src/components/config/config-split-page.tsx +109 -0
  119. package/src/components/config/desktop-update-config.test.tsx +10 -4
  120. package/src/components/config/desktop-update-config.tsx +5 -3
  121. package/src/components/config/provider-enabled-field.tsx +17 -10
  122. package/src/components/config/runtime-control-card.test.tsx +136 -158
  123. package/src/components/config/runtime-control-card.tsx +43 -68
  124. package/src/components/config/runtime-presence-card.test.tsx +10 -14
  125. package/src/components/config/runtime-presence-card.tsx +97 -81
  126. package/src/components/layout/AppLayout.tsx +25 -37
  127. package/src/components/layout/Sidebar.tsx +4 -4
  128. package/src/components/layout/app-layout.test.tsx +46 -14
  129. package/src/components/layout/runtime-status-entry.test.tsx +101 -0
  130. package/src/components/layout/runtime-status-entry.tsx +95 -0
  131. package/src/components/layout/sidebar.layout.test.tsx +11 -5
  132. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  133. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  134. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  135. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  136. package/src/components/marketplace/marketplace-page.tsx +596 -0
  137. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  138. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  139. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  140. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  141. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  142. package/src/components/ui/notice-card.tsx +129 -0
  143. package/src/components/ui/setting-row.tsx +51 -0
  144. package/src/components/ui/tag-chip.tsx +39 -0
  145. package/src/components/ui/textarea.tsx +19 -0
  146. package/src/features/account/components/account-panel.tsx +255 -0
  147. package/src/features/account/index.ts +6 -0
  148. package/src/{account → features/account}/managers/account.manager.ts +6 -5
  149. package/src/features/remote/components/remote-access-page.test.tsx +104 -0
  150. package/src/features/remote/components/remote-access-page.tsx +250 -0
  151. package/src/{hooks/useRemoteAccess.ts → features/remote/hooks/use-remote-access.ts} +1 -1
  152. package/src/features/remote/index.ts +27 -0
  153. package/src/{remote → features/remote}/managers/remote-access.manager.ts +3 -4
  154. package/src/{remote → features/remote/services}/remote-access-feedback.service.test.ts +1 -1
  155. package/src/features/system-status/hooks/use-system-status.ts +104 -0
  156. package/src/features/system-status/index.ts +12 -0
  157. package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +126 -0
  158. package/src/features/system-status/managers/system-status.manager.test.ts +142 -0
  159. package/src/features/system-status/managers/system-status.manager.ts +511 -0
  160. package/src/features/system-status/stores/system-status.store.ts +32 -0
  161. package/src/features/system-status/types/system-status.types.ts +73 -0
  162. package/src/features/system-status/utils/system-status.utils.test.ts +132 -0
  163. package/src/features/system-status/utils/system-status.utils.ts +202 -0
  164. package/src/hooks/use-realtime-query-bridge.ts +34 -18
  165. package/src/hooks/useConfig.ts +2 -1
  166. package/src/index.css +24 -0
  167. package/src/lib/app-resource-uri.test.ts +20 -0
  168. package/src/lib/app-resource-uri.ts +29 -0
  169. package/src/lib/i18n.chat.ts +8 -0
  170. package/src/lib/i18n.remote.ts +1 -1
  171. package/src/lib/i18n.runtime-control.ts +31 -0
  172. package/src/lib/i18n.ts +5 -8
  173. package/src/lib/session-context.utils.test.ts +71 -0
  174. package/src/lib/session-context.utils.ts +28 -3
  175. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  176. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  177. package/src/platforms/desktop/index.ts +20 -0
  178. package/src/{desktop → platforms/desktop}/managers/desktop-presence.manager.ts +2 -2
  179. package/src/{desktop → platforms/desktop}/managers/desktop-update.manager.ts +2 -2
  180. package/src/{desktop → platforms/desktop}/stores/desktop-presence.store.ts +1 -1
  181. package/src/{desktop → platforms/desktop}/stores/desktop-update.store.ts +1 -1
  182. package/src/stores/ui.store.ts +0 -9
  183. package/src/transport/{app-client.ts → app-client.service.ts} +9 -9
  184. package/src/transport/app-client.test.ts +9 -5
  185. package/src/transport/index.ts +1 -1
  186. package/src/transport/{local.transport.ts → local-transport.service.ts} +14 -12
  187. package/dist/assets/ChannelsList-Ita2Zm1_.js +0 -8
  188. package/dist/assets/DocBrowser-BNwbPHf4.js +0 -1
  189. package/dist/assets/MarketplacePage-CjX2MWww.js +0 -1
  190. package/dist/assets/MarketplacePage-D0sDlYX4.js +0 -49
  191. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +0 -40
  192. package/dist/assets/ModelConfig-BzZenCH-.js +0 -1
  193. package/dist/assets/ProviderScopedModelInput-Da7khnBA.js +0 -1
  194. package/dist/assets/ProvidersList-BbVzRxjY.js +0 -1
  195. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +0 -1
  196. package/dist/assets/RuntimeConfig-F_XKGgLm.js +0 -1
  197. package/dist/assets/SearchConfig-BGkzXQP-.js +0 -1
  198. package/dist/assets/SecretsConfig-D281Rotl.js +0 -3
  199. package/dist/assets/app-query-client-VnFElj4E.js +0 -1
  200. package/dist/assets/chat-page-Doe0yTtB.js +0 -58
  201. package/dist/assets/chat-session-display-cw78aiI_.js +0 -1
  202. package/dist/assets/client-_i4MU2bB.js +0 -7
  203. package/dist/assets/config-DtIQwrHF.js +0 -1
  204. package/dist/assets/config-layout-CHs0mAaR.js +0 -1
  205. package/dist/assets/desktop-update-config-Dpcf4BKG.js +0 -1
  206. package/dist/assets/dist-ccBFUi-o.js +0 -9
  207. package/dist/assets/index-CF9xve0E.js +0 -6
  208. package/dist/assets/index-FgA52VBt.css +0 -1
  209. package/dist/assets/infiniteQueryBehavior-ZDS92Qpp.js +0 -1
  210. package/dist/assets/loader-circle-ACM1s51e.js +0 -1
  211. package/dist/assets/page-layout-vZnghcFy.js +0 -1
  212. package/dist/assets/play-CFUwCA2E.js +0 -1
  213. package/dist/assets/plus-rYsv72JG.js +0 -1
  214. package/dist/assets/popover-Bg1VoTZ6.js +0 -1
  215. package/dist/assets/refresh-ccw-DT98i__E.js +0 -1
  216. package/dist/assets/rotate-cw-JtFzpNn6.js +0 -1
  217. package/dist/assets/search-3kFR_zh9.js +0 -1
  218. package/dist/assets/security-config-BWaiARNk.js +0 -1
  219. package/dist/assets/select-DJ2MUjBB.js +0 -41
  220. package/dist/assets/skeleton-ByQepn0M.js +0 -1
  221. package/dist/assets/status-dot-vbanNPFU.js +0 -1
  222. package/dist/assets/use-infinite-scroll-loader-DkNhD-42.js +0 -1
  223. package/dist/assets/useConfirmDialog-BkvTN-vd.js +0 -1
  224. package/dist/assets/x-ByDbItbq.js +0 -1
  225. package/src/account/components/account-panel.tsx +0 -135
  226. package/src/components/agents/AgentDialogs.tsx +0 -400
  227. package/src/components/agents/AgentsPage.test.tsx +0 -217
  228. package/src/components/agents/AgentsPage.tsx +0 -352
  229. package/src/components/config/config-layout.ts +0 -10
  230. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  231. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  232. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  233. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  234. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  235. package/src/components/remote/RemoteAccessPage.tsx +0 -144
  236. package/src/hooks/use-runtime-control.ts +0 -24
  237. package/src/presenter/app-presenter-context.tsx +0 -20
  238. package/src/presenter/app.presenter.ts +0 -12
  239. package/src/runtime-control/runtime-control.manager.ts +0 -118
  240. /package/dist/assets/{config-hints-BhTmc9P1.js → config-hints-DSQQbeOA.js} +0 -0
  241. /package/src/{account → features/account}/stores/account.store.ts +0 -0
  242. /package/src/{remote → features/remote/services}/remote-access-feedback.service.ts +0 -0
  243. /package/src/{remote/remote-access.query.ts → features/remote/services/remote-access-query.service.ts} +0 -0
  244. /package/src/{remote → features/remote}/stores/remote-access.store.ts +0 -0
  245. /package/src/{desktop → platforms/desktop/types}/desktop-update.types.ts +0 -0
@@ -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
+ }
@@ -0,0 +1,20 @@
1
+ export {
2
+ desktopPresenceManager,
3
+ DesktopPresenceManager,
4
+ } from './managers/desktop-presence.manager';
5
+ export {
6
+ desktopUpdateManager,
7
+ DesktopUpdateManager,
8
+ } from './managers/desktop-update.manager';
9
+ export { useDesktopPresenceStore } from './stores/desktop-presence.store';
10
+ export { useDesktopUpdateStore } from './stores/desktop-update.store';
11
+ export type {
12
+ DesktopPresencePreferences,
13
+ DesktopPresenceSnapshot,
14
+ DesktopReleaseChannel,
15
+ DesktopRuntimeControlResult,
16
+ DesktopUpdatePreferences,
17
+ DesktopUpdateSnapshot,
18
+ DesktopUpdateStatus,
19
+ NextClawDesktopBridge,
20
+ } from './types/desktop-update.types';
@@ -1,8 +1,8 @@
1
1
  import type {
2
2
  DesktopPresencePreferences,
3
3
  NextClawDesktopBridge
4
- } from '@/desktop/desktop-update.types';
5
- import { useDesktopPresenceStore } from '@/desktop/stores/desktop-presence.store';
4
+ } from '@/platforms/desktop/types/desktop-update.types';
5
+ import { useDesktopPresenceStore } from '@/platforms/desktop/stores/desktop-presence.store';
6
6
  import { t } from '@/lib/i18n';
7
7
  import { toast } from 'sonner';
8
8
 
@@ -3,8 +3,8 @@ import type {
3
3
  DesktopUpdatePreferences,
4
4
  DesktopUpdateSnapshot,
5
5
  NextClawDesktopBridge
6
- } from '@/desktop/desktop-update.types';
7
- import { useDesktopUpdateStore } from '@/desktop/stores/desktop-update.store';
6
+ } from '@/platforms/desktop/types/desktop-update.types';
7
+ import { useDesktopUpdateStore } from '@/platforms/desktop/stores/desktop-update.store';
8
8
  import { t } from '@/lib/i18n';
9
9
  import { toast } from 'sonner';
10
10
 
@@ -1,4 +1,4 @@
1
- import type { DesktopPresenceSnapshot } from '@/desktop/desktop-update.types';
1
+ import type { DesktopPresenceSnapshot } from '@/platforms/desktop/types/desktop-update.types';
2
2
  import { create } from 'zustand';
3
3
 
4
4
  type DesktopPresenceBusyAction = 'loading' | 'saving-preferences' | null;
@@ -1,4 +1,4 @@
1
- import type { DesktopUpdateSnapshot } from '@/desktop/desktop-update.types';
1
+ import type { DesktopUpdateSnapshot } from '@/platforms/desktop/types/desktop-update.types';
2
2
  import { create } from 'zustand';
3
3
 
4
4
  type DesktopUpdateBusyAction =
@@ -1,12 +1,6 @@
1
1
  import { create } from 'zustand';
2
2
 
3
- type ConnectionStatus = 'connected' | 'disconnected' | 'connecting';
4
-
5
3
  interface UiState {
6
- // Connection status
7
- connectionStatus: ConnectionStatus;
8
- setConnectionStatus: (status: ConnectionStatus) => void;
9
-
10
4
  // Channel modal
11
5
  channelModal: { open: boolean; channel?: string };
12
6
  openChannelModal: (channel?: string) => void;
@@ -14,9 +8,6 @@ interface UiState {
14
8
  }
15
9
 
16
10
  export const useUiStore = create<UiState>((set) => ({
17
- connectionStatus: 'disconnected',
18
- setConnectionStatus: (status) => set({ connectionStatus: status }),
19
-
20
11
  channelModal: { open: false },
21
12
  openChannelModal: (channel) => set({ channelModal: { open: true, channel } }),
22
13
  closeChannelModal: () => set({ channelModal: { open: false } })
@@ -1,5 +1,5 @@
1
1
  import { API_BASE } from '@/api/api-base';
2
- import { LocalAppTransport } from './local.transport';
2
+ import { LocalAppTransport } from './local-transport.service';
3
3
  import { RemoteSessionMultiplexTransport } from './remote.transport';
4
4
  import type { AppTransport, RemoteRuntimeInfo, RequestInput, StreamInput, StreamSession } from './transport.types';
5
5
 
@@ -59,18 +59,18 @@ class AppClient {
59
59
 
60
60
  constructor(private readonly apiBase: string = API_BASE) {}
61
61
 
62
- private async getTransport(): Promise<AppTransport> {
62
+ private getTransport = async (): Promise<AppTransport> => {
63
63
  if (!this.transportPromise) {
64
64
  this.transportPromise = resolveRuntime(this.apiBase);
65
65
  }
66
66
  return await this.transportPromise;
67
- }
67
+ };
68
68
 
69
- async request<T>(input: RequestInput): Promise<T> {
69
+ request = async <T>(input: RequestInput): Promise<T> => {
70
70
  return await (await this.getTransport()).request<T>(input);
71
- }
71
+ };
72
72
 
73
- openStream<TFinal = unknown>(input: StreamInput): StreamSession<TFinal> {
73
+ openStream = <TFinal = unknown>(input: StreamInput): StreamSession<TFinal> => {
74
74
  let currentSession: StreamSession<TFinal> | null = null;
75
75
  let resolveFinished!: (value: TFinal) => void;
76
76
  let rejectFinished!: (error: Error) => void;
@@ -94,9 +94,9 @@ class AppClient {
94
94
  finished,
95
95
  cancel: () => currentSession?.cancel()
96
96
  };
97
- }
97
+ };
98
98
 
99
- subscribe(handler: (event: Parameters<Parameters<AppTransport['subscribe']>[0]>[0]) => void): () => void {
99
+ subscribe = (handler: (event: Parameters<Parameters<AppTransport['subscribe']>[0]>[0]) => void): () => void => {
100
100
  let unsubscribe = () => {};
101
101
  let active = true;
102
102
  void this.getTransport().then((transport) => {
@@ -117,7 +117,7 @@ class AppClient {
117
117
  active = false;
118
118
  unsubscribe();
119
119
  };
120
- }
120
+ };
121
121
  }
122
122
 
123
123
  export const appClient = new AppClient();
@@ -21,12 +21,16 @@ describe('appClient runtime detection', () => {
21
21
  );
22
22
  vi.stubGlobal('fetch', fetchMock);
23
23
 
24
- const { LocalAppTransport } = await import('@/transport/local.transport');
25
- const localRequest = vi
26
- .spyOn(LocalAppTransport.prototype, 'request')
27
- .mockResolvedValue({ ok: true } as never);
24
+ const localRequest = vi.fn().mockResolvedValue({ ok: true } as never);
25
+ vi.doMock('@/transport/local-transport.service', () => ({
26
+ LocalAppTransport: class {
27
+ request = localRequest;
28
+ openStream = vi.fn();
29
+ subscribe = vi.fn();
30
+ }
31
+ }));
28
32
 
29
- const { appClient } = await import('@/transport/app-client');
33
+ const { appClient } = await import('@/transport/app-client.service');
30
34
  const result = await appClient.request<{ ok: boolean }>({
31
35
  method: 'GET',
32
36
  path: '/api/config'
@@ -1,4 +1,4 @@
1
- export { appClient } from './app-client';
1
+ export { appClient } from './app-client.service';
2
2
  export type {
3
3
  AppEvent,
4
4
  AppTransport,
@@ -1,6 +1,7 @@
1
1
  import { API_BASE } from '@/api/api-base';
2
- import { requestRawApiResponse } from '@/api/raw-client';
2
+ import { requestRawApiResponse } from '@/api/raw-client.utils';
3
3
  import type { ApiResponse } from '@/api/types';
4
+ import { systemStatusManager } from '@/features/system-status';
4
5
  import type { AppEvent, AppTransport, RequestInput, StreamInput, StreamSession } from './transport.types';
5
6
  import { readSseStreamResult } from './sse-stream';
6
7
  import { resolveTransportWebSocketUrl } from './transport-websocket-url';
@@ -42,7 +43,7 @@ class LocalRealtimeGateway {
42
43
 
43
44
  constructor(private readonly wsUrl: string) {}
44
45
 
45
- subscribe(handler: EventHandler): () => void {
46
+ subscribe = (handler: EventHandler): () => void => {
46
47
  this.subscribers.add(handler);
47
48
  if (this.subscribers.size === 1) {
48
49
  this.connect();
@@ -56,15 +57,15 @@ class LocalRealtimeGateway {
56
57
  this.disconnect();
57
58
  }
58
59
  };
59
- }
60
+ };
60
61
 
61
- private emit(event: AppEvent): void {
62
+ private emit = (event: AppEvent): void => {
62
63
  for (const subscriber of this.subscribers) {
63
64
  subscriber(event);
64
65
  }
65
- }
66
+ };
66
67
 
67
- private connect(): void {
68
+ private connect = (): void => {
68
69
  if (this.socket && (this.socket.readyState === WebSocket.CONNECTING || this.socket.readyState === WebSocket.OPEN)) {
69
70
  return;
70
71
  }
@@ -96,9 +97,9 @@ class LocalRealtimeGateway {
96
97
  this.scheduleReconnect();
97
98
  }
98
99
  };
99
- }
100
+ };
100
101
 
101
- private scheduleReconnect(): void {
102
+ private scheduleReconnect = (): void => {
102
103
  if (this.reconnectTimer !== null) {
103
104
  return;
104
105
  }
@@ -106,9 +107,9 @@ class LocalRealtimeGateway {
106
107
  this.reconnectTimer = null;
107
108
  this.connect();
108
109
  }, 3_000);
109
- }
110
+ };
110
111
 
111
- private disconnect(): void {
112
+ private disconnect = (): void => {
112
113
  this.manualClose = true;
113
114
  if (this.reconnectTimer !== null) {
114
115
  window.clearTimeout(this.reconnectTimer);
@@ -116,7 +117,7 @@ class LocalRealtimeGateway {
116
117
  }
117
118
  this.socket?.close();
118
119
  this.socket = null;
119
- }
120
+ };
120
121
  }
121
122
 
122
123
  export class LocalAppTransport implements AppTransport {
@@ -154,7 +155,7 @@ export class LocalAppTransport implements AppTransport {
154
155
  return response.data;
155
156
  } catch (error) {
156
157
  if (controller?.signal.aborted) {
157
- const reason = controller.signal.reason;
158
+ const { reason } = controller.signal;
158
159
  throw new Error(typeof reason === 'string' && reason.trim() ? reason : `Request timed out: ${input.method} ${input.path}`);
159
160
  }
160
161
  if (error instanceof Error) {
@@ -196,6 +197,7 @@ export class LocalAppTransport implements AppTransport {
196
197
  signal: controller.signal
197
198
  });
198
199
  } catch (error) {
200
+ systemStatusManager.reportTransportFailure(formatUnknownTransportError(error));
199
201
  throw createErrorWithCause(
200
202
  `Stream request failed for ${input.method} ${input.path} | ${formatUnknownTransportError(error)}`,
201
203
  error