@nextclaw/ui 0.12.9 → 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 (178) hide show
  1. package/CHANGELOG.md +61 -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-6ReNjvzF.js → DocBrowser-DMfr0Oow.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-B6SpA7Qs.js → DocBrowserContext-BXydqby-.js} +1 -1
  6. package/dist/assets/{LogoBadge-ByNLYg65.js → LogoBadge-hO7tY7hE.js} +1 -1
  7. package/dist/assets/ModelConfig-CNIgLf0e.js +1 -0
  8. package/dist/assets/{ProviderScopedModelInput-Da7khnBA.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-D281Rotl.js → SecretsConfig-MEt6MjuD.js} +2 -2
  13. package/dist/assets/SessionsConfig-DifCiXwR.js +2 -0
  14. package/dist/assets/{app-query-client-VnFElj4E.js → app-query-client-9jNewezV.js} +1 -1
  15. package/dist/assets/{book-open-BdcxxoQu.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-DK5HPmIK.js → chunk-JZWAC4HX-C5dEc8hV.js} +1 -1
  19. package/dist/assets/{client-_i4MU2bB.js → client-C-8fH7-c.js} +1 -1
  20. package/dist/assets/{config-DtIQwrHF.js → config-CBScxsdV.js} +1 -1
  21. package/dist/assets/config-split-page-BUout_Ak.js +1 -0
  22. package/dist/assets/{createLucideIcon-BSeTgkZW.js → createLucideIcon-dy5ie7Ox.js} +1 -1
  23. package/dist/assets/desktop-update-config-2BS6BMkW.js +1 -0
  24. package/dist/assets/{dist-ccBFUi-o.js → dist-BruyLa92.js} +1 -1
  25. package/dist/assets/{dist-6TrrnPCR.js → dist-Cy7_j6hA.js} +1 -1
  26. package/dist/assets/{download-BhDxnyvU.js → download-BD0ETkB-.js} +1 -1
  27. package/dist/assets/{external-link-BgErLCNT.js → external-link-kZSAO8nT.js} +1 -1
  28. package/dist/assets/{hash-Bl7dr_UG.js → hash-BHJC2Ovu.js} +1 -1
  29. package/dist/assets/{i18n-eDHeDY0n.js → i18n-CpTZLchQ.js} +1 -1
  30. package/dist/assets/index-mW8W2FUu.css +1 -0
  31. package/dist/assets/index-zDZfXoI4.js +6 -0
  32. package/dist/assets/{infiniteQueryBehavior-ZDS92Qpp.js → infiniteQueryBehavior-CyER9hv0.js} +1 -1
  33. package/dist/assets/loader-circle-Bc2gCU33.js +1 -0
  34. package/dist/assets/{logos-x89HbrZ4.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-vZnghcFy.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-DT98i__E.js → refresh-ccw-COVhNHtN.js} +1 -1
  42. package/dist/assets/{refresh-cw-C47QSEwg.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-JtFzpNn6.js → rotate-cw-oHMKJMC8.js} +1 -1
  45. package/dist/assets/{save-3S6-H3Xw.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-vbanNPFU.js → status-dot-DpPtVzQT.js} +1 -1
  52. package/dist/assets/{switch-BsLtHOH-.js → switch-CM29eCAR.js} +1 -1
  53. package/dist/assets/{tabs-custom-D3HYMt6k.js → tabs-custom-YcZUWn3o.js} +1 -1
  54. package/dist/assets/tag-chip-DMXdnLcj.js +1 -0
  55. package/dist/assets/{trash-2-G48scll7.js → trash-2-mJT6oWa2.js} +1 -1
  56. package/dist/assets/{use-infinite-scroll-loader-DkNhD-42.js → use-infinite-scroll-loader-DJ1L81Dz.js} +1 -1
  57. package/dist/assets/{useConfirmDialog-BkvTN-vd.js → useConfirmDialog-BsVuqu1x.js} +1 -1
  58. package/dist/assets/{useMutation-CBWjE2uj.js → useMutation-CNcz2fgt.js} +1 -1
  59. package/dist/assets/x-Czwxm82I.js +1 -0
  60. package/dist/index.html +22 -22
  61. package/dist/runtime-icons/claude.ico +0 -0
  62. package/dist/runtime-icons/codex-openai.svg +6 -0
  63. package/dist/runtime-icons/hermes-agent.png +0 -0
  64. package/package.json +6 -6
  65. package/public/runtime-icons/claude.ico +0 -0
  66. package/public/runtime-icons/codex-openai.svg +6 -0
  67. package/public/runtime-icons/hermes-agent.png +0 -0
  68. package/src/account/components/account-panel.tsx +217 -97
  69. package/src/account/managers/account.manager.ts +3 -2
  70. package/src/api/chat-session-type.types.ts +7 -0
  71. package/src/api/runtime-control.types.ts +8 -0
  72. package/src/api/types.ts +8 -0
  73. package/src/app.tsx +221 -57
  74. package/src/components/agents/agent-dialogs.tsx +499 -0
  75. package/src/components/agents/agents-page.test.tsx +238 -0
  76. package/src/components/agents/agents-page.tsx +435 -0
  77. package/src/components/chat/ChatSidebar.tsx +11 -35
  78. package/src/components/chat/chat-conversation-panel.test.tsx +20 -0
  79. package/src/components/chat/chat-conversation-panel.tsx +83 -13
  80. package/src/components/chat/chat-page-shell.tsx +19 -13
  81. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  82. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  83. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +87 -0
  84. package/src/components/chat/chat-session-workspace-file-preview.tsx +14 -43
  85. package/src/components/chat/chat-session-workspace-panel-nav.tsx +8 -2
  86. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  87. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  88. package/src/components/chat/ncp/ncp-chat-page.tsx +2 -0
  89. package/src/components/chat/ncp/ncp-session-adapter.test.ts +1 -0
  90. package/src/components/chat/ncp/ncp-session-adapter.ts +3 -0
  91. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +10 -4
  92. package/src/components/chat/stores/chat-input.store.ts +2 -1
  93. package/src/components/chat/stores/chat-thread.store.ts +3 -1
  94. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  95. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  96. package/src/components/common/BrandHeader.tsx +3 -1
  97. package/src/components/common/session-context-icon.tsx +15 -2
  98. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  99. package/src/components/config/ChannelForm.test.tsx +89 -3
  100. package/src/components/config/ChannelForm.tsx +157 -188
  101. package/src/components/config/ChannelsList.test.tsx +163 -119
  102. package/src/components/config/ChannelsList.tsx +90 -101
  103. package/src/components/config/ProviderForm.tsx +108 -146
  104. package/src/components/config/ProvidersList.tsx +100 -123
  105. package/src/components/config/SearchConfig.tsx +423 -393
  106. package/src/components/config/channel-form-fields-section.tsx +70 -37
  107. package/src/components/config/config-split-page.tsx +109 -0
  108. package/src/components/config/provider-enabled-field.tsx +17 -10
  109. package/src/components/config/runtime-control-card.test.tsx +56 -0
  110. package/src/components/config/runtime-control-card.tsx +25 -0
  111. package/src/components/config/runtime-presence-card.tsx +93 -79
  112. package/src/components/layout/AppLayout.tsx +25 -37
  113. package/src/components/layout/app-layout.test.tsx +46 -14
  114. package/src/components/layout/runtime-status-entry.test.tsx +157 -0
  115. package/src/components/layout/runtime-status-entry.tsx +143 -0
  116. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  117. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  118. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  119. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  120. package/src/components/marketplace/marketplace-page.tsx +596 -0
  121. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  122. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  123. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  124. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  125. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  126. package/src/components/remote/remote-access-page.test.tsx +105 -0
  127. package/src/components/remote/remote-access-page.tsx +248 -0
  128. package/src/components/ui/notice-card.tsx +129 -0
  129. package/src/components/ui/setting-row.tsx +51 -0
  130. package/src/components/ui/tag-chip.tsx +39 -0
  131. package/src/components/ui/textarea.tsx +19 -0
  132. package/src/hooks/useConfig.ts +2 -1
  133. package/src/index.css +24 -0
  134. package/src/lib/app-resource-uri.test.ts +20 -0
  135. package/src/lib/app-resource-uri.ts +29 -0
  136. package/src/lib/i18n.remote.ts +1 -1
  137. package/src/lib/i18n.runtime-control.ts +31 -0
  138. package/src/lib/i18n.ts +5 -8
  139. package/src/lib/session-context.utils.test.ts +71 -0
  140. package/src/lib/session-context.utils.ts +28 -3
  141. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  142. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  143. package/dist/assets/ChannelsList-Ita2Zm1_.js +0 -8
  144. package/dist/assets/DocBrowser-BNwbPHf4.js +0 -1
  145. package/dist/assets/MarketplacePage-CjX2MWww.js +0 -1
  146. package/dist/assets/MarketplacePage-D0sDlYX4.js +0 -49
  147. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +0 -40
  148. package/dist/assets/ModelConfig-BzZenCH-.js +0 -1
  149. package/dist/assets/ProvidersList-BbVzRxjY.js +0 -1
  150. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +0 -1
  151. package/dist/assets/RuntimeConfig-F_XKGgLm.js +0 -1
  152. package/dist/assets/SearchConfig-BGkzXQP-.js +0 -1
  153. package/dist/assets/SessionsConfig-ChHQ7M5c.js +0 -2
  154. package/dist/assets/chat-page-Doe0yTtB.js +0 -58
  155. package/dist/assets/chat-session-display-cw78aiI_.js +0 -1
  156. package/dist/assets/config-layout-CHs0mAaR.js +0 -1
  157. package/dist/assets/desktop-update-config-Dpcf4BKG.js +0 -1
  158. package/dist/assets/index-CF9xve0E.js +0 -6
  159. package/dist/assets/index-FgA52VBt.css +0 -1
  160. package/dist/assets/loader-circle-ACM1s51e.js +0 -1
  161. package/dist/assets/play-CFUwCA2E.js +0 -1
  162. package/dist/assets/plus-rYsv72JG.js +0 -1
  163. package/dist/assets/popover-Bg1VoTZ6.js +0 -1
  164. package/dist/assets/search-3kFR_zh9.js +0 -1
  165. package/dist/assets/security-config-BWaiARNk.js +0 -1
  166. package/dist/assets/select-DJ2MUjBB.js +0 -41
  167. package/dist/assets/skeleton-ByQepn0M.js +0 -1
  168. package/dist/assets/x-ByDbItbq.js +0 -1
  169. package/src/components/agents/AgentDialogs.tsx +0 -400
  170. package/src/components/agents/AgentsPage.test.tsx +0 -217
  171. package/src/components/agents/AgentsPage.tsx +0 -352
  172. package/src/components/config/config-layout.ts +0 -10
  173. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  174. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  175. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  176. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  177. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  178. package/src/components/remote/RemoteAccessPage.tsx +0 -144
@@ -1,11 +1,11 @@
1
1
  import { useMemo, useState, type ReactNode } from 'react';
2
2
  import { Plus } from 'lucide-react';
3
3
  import { Button } from '@/components/ui/button';
4
+ import { ChatSessionTypeOptionItem } from '@/components/chat/chat-session-type-option-item';
4
5
  import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
5
6
  import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
6
7
  import type { NcpSessionListItemView } from '@/components/chat/ncp/use-ncp-session-list-view';
7
8
  import { t } from '@/lib/i18n';
8
- import { cn } from '@/lib/utils';
9
9
 
10
10
  export type ChatSidebarProjectGroup = {
11
11
  projectRoot: string;
@@ -34,16 +34,6 @@ function resolveProjectGroupDefaultSessionType(
34
34
  return sessionTypeOptions[0]?.value ?? defaultSessionType;
35
35
  }
36
36
 
37
- function resolveSessionTypeStatusText(option: {
38
- ready?: boolean;
39
- reasonMessage?: string | null;
40
- }): string {
41
- if (option.ready === false) {
42
- return option.reasonMessage?.trim() || t('statusSetup');
43
- }
44
- return t('statusReady');
45
- }
46
-
47
37
  export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
48
38
  const { groups, defaultSessionType, sessionTypeOptions, renderSessionItem, onCreateSession } = props;
49
39
  const [openProjectRoot, setOpenProjectRoot] = useState<string | null>(null);
@@ -91,38 +81,23 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
91
81
  <Plus className="h-3.5 w-3.5" />
92
82
  </Button>
93
83
  </PopoverTrigger>
94
- <PopoverContent align="end" className="w-64 p-2">
95
- <div className="px-2 py-1 text-[11px] font-medium uppercase tracking-wider text-gray-400">
84
+ <PopoverContent
85
+ align="end"
86
+ className="w-56 rounded-2xl border border-gray-200/80 bg-white p-1.5 shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
87
+ >
88
+ <div className="px-3 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-[0.18em] text-gray-400">
96
89
  {t('chatSessionTypeLabel')}
97
90
  </div>
98
- <div className="mt-1 space-y-1">
91
+ <div className="space-y-1">
99
92
  {sessionTypeOptions.map((option) => (
100
- <button
93
+ <ChatSessionTypeOptionItem
101
94
  key={`${group.projectRoot}:${option.value}`}
102
- type="button"
103
- onClick={() => {
95
+ option={option}
96
+ onSelect={() => {
104
97
  onCreateSession(option.value, group.projectRoot);
105
98
  setOpenProjectRoot(null);
106
99
  }}
107
- className="w-full rounded-xl px-3 py-2 text-left transition-colors hover:bg-gray-100"
108
- >
109
- <div className="flex items-center justify-between gap-3">
110
- <div className="text-[13px] font-medium text-gray-900">{option.label}</div>
111
- <span
112
- className={cn(
113
- 'shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold',
114
- option.ready === false
115
- ? 'bg-amber-100 text-amber-800'
116
- : 'bg-emerald-100 text-emerald-700'
117
- )}
118
- >
119
- {option.ready === false ? t('statusSetup') : t('statusReady')}
120
- </span>
121
- </div>
122
- <div className="mt-0.5 text-[11px] text-gray-500">
123
- {resolveSessionTypeStatusText(option)}
124
- </div>
125
- </button>
100
+ />
126
101
  ))}
127
102
  </div>
128
103
  </PopoverContent>
@@ -0,0 +1,77 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { adaptChatMessage } from '@/components/chat/adapters/chat-message.adapter';
3
+ import { adaptNcpMessageToUiMessage } from '../ncp-session-adapter';
4
+
5
+ const texts = {
6
+ roleLabels: {
7
+ user: 'User',
8
+ assistant: 'Assistant',
9
+ tool: 'Tool',
10
+ system: 'System',
11
+ fallback: 'Message',
12
+ },
13
+ reasoningLabel: 'Reasoning',
14
+ toolCallLabel: 'Tool Call',
15
+ toolResultLabel: 'Tool Result',
16
+ toolInputLabel: 'Input',
17
+ toolNoOutputLabel: 'No output',
18
+ toolOutputLabel: 'Output',
19
+ toolStatusPreparingLabel: 'Preparing',
20
+ toolStatusRunningLabel: 'Running',
21
+ toolStatusCompletedLabel: 'Completed',
22
+ toolStatusFailedLabel: 'Failed',
23
+ toolStatusCancelledLabel: 'Cancelled',
24
+ imageAttachmentLabel: 'Image',
25
+ fileAttachmentLabel: 'File',
26
+ unknownPartLabel: 'Unknown',
27
+ };
28
+
29
+ describe('adaptNcpMessageToUiMessage cancelled tools', () => {
30
+ it('renders cancelled tool invocations as cancelled cards', () => {
31
+ const uiMessage = adaptNcpMessageToUiMessage({
32
+ id: 'ncp-message-tool-cancelled-1',
33
+ sessionId: 'ncp-session-1',
34
+ role: 'assistant',
35
+ status: 'final',
36
+ timestamp: '2026-04-01T00:00:00.000Z',
37
+ parts: [
38
+ {
39
+ type: 'tool-invocation',
40
+ toolCallId: 'tool-cancelled-1',
41
+ toolName: 'write_file',
42
+ state: 'cancelled',
43
+ args: JSON.stringify({
44
+ path: 'src/app.ts',
45
+ content: 'hello',
46
+ }),
47
+ },
48
+ ],
49
+ });
50
+
51
+ const adapted = adaptChatMessage(
52
+ {
53
+ id: uiMessage.id,
54
+ role: uiMessage.role,
55
+ meta: {
56
+ timestamp: uiMessage.meta?.timestamp,
57
+ status: uiMessage.meta?.status,
58
+ },
59
+ parts: uiMessage.parts as never,
60
+ },
61
+ {
62
+ formatTimestamp: (value) => value ?? '',
63
+ texts,
64
+ },
65
+ );
66
+
67
+ expect(adapted.parts[0]).toMatchObject({
68
+ type: 'tool-card',
69
+ card: {
70
+ toolName: 'write_file',
71
+ statusTone: 'cancelled',
72
+ statusLabel: 'Cancelled',
73
+ titleLabel: 'Tool Result',
74
+ },
75
+ });
76
+ });
77
+ });
@@ -304,6 +304,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
304
304
  currentAgent,
305
305
  parentSession,
306
306
  currentSessionTypeLabel,
307
+ currentSessionTypeIcon,
307
308
  currentChildSessionTabs,
308
309
  } = useNcpChatDerivedState({
309
310
  sessionKey,
@@ -340,6 +341,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
340
341
  isSkillsLoading: sessionSkillsQuery.isLoading,
341
342
  sessionTypeUnavailableMessage,
342
343
  currentSessionTypeLabel,
344
+ currentSessionTypeIcon,
343
345
  sessionKey,
344
346
  currentAgentId,
345
347
  currentAgent,
@@ -316,6 +316,7 @@ describe('adaptNcpMessageToUiMessage tool rendering', () => {
316
316
  },
317
317
  });
318
318
  });
319
+
319
320
  });
320
321
 
321
322
  describe('readNcpSessionPreferredThinking', () => {
@@ -151,6 +151,9 @@ function parseSessionContext(sessionKey: string): { channel?: string; type?: str
151
151
  }
152
152
 
153
153
  function mapToolStatus(part: Extract<NcpMessagePart, { type: 'tool-invocation' }>): ToolInvocationStatus {
154
+ if (part.state === 'cancelled') {
155
+ return ToolInvocationStatus.CANCELLED;
156
+ }
154
157
  if (part.state === 'result') {
155
158
  return ToolInvocationStatus.RESULT;
156
159
  }
@@ -11,6 +11,7 @@ import type { NcpChatPresenter } from '@/components/chat/ncp/ncp-chat.presenter'
11
11
  import type { UseHydratedNcpAgentResult } from '@nextclaw/ncp-react';
12
12
  import type { ChatModelOption } from '@/components/chat/chat-input.types';
13
13
  import type { ChatChildSessionTab } from '@/components/chat/stores/chat-thread.store';
14
+ import type { ChatSessionTypeOption } from '@/components/chat/useChatSessionTypeState';
14
15
  import { resolveSessionTypeLabel } from '@/components/chat/useChatSessionTypeState';
15
16
 
16
17
  function buildChildSessionTabs(params: {
@@ -40,7 +41,7 @@ export function useNcpChatDerivedState(params: {
40
41
  parentSessionId: string | null;
41
42
  sessionSummaries: NcpSessionSummaryView[];
42
43
  selectedSessionType: string;
43
- sessionTypeOptions: Array<{ value: string; label: string }>;
44
+ sessionTypeOptions: ChatSessionTypeOption[];
44
45
  }) {
45
46
  const {
46
47
  availableAgents,
@@ -68,9 +69,11 @@ export function useNcpChatDerivedState(params: {
68
69
  ) ?? null;
69
70
  return parentSummary ? adaptNcpSessionSummary(parentSummary) : null;
70
71
  }, [parentSessionId, sessionSummaries]);
72
+ const currentSessionTypeOption =
73
+ sessionTypeOptions.find((option) => option.value === selectedSessionType) ?? null;
71
74
  const currentSessionTypeLabel =
72
- sessionTypeOptions.find((option) => option.value === selectedSessionType)
73
- ?.label ?? resolveSessionTypeLabel(selectedSessionType);
75
+ currentSessionTypeOption?.label ?? resolveSessionTypeLabel(selectedSessionType);
76
+ const currentSessionTypeIcon = currentSessionTypeOption?.icon ?? null;
74
77
  const currentChildSessionTabs = useMemo(
75
78
  () =>
76
79
  buildChildSessionTabs({
@@ -86,6 +89,7 @@ export function useNcpChatDerivedState(params: {
86
89
  currentAgent,
87
90
  parentSession,
88
91
  currentSessionTypeLabel,
92
+ currentSessionTypeIcon,
89
93
  currentChildSessionTabs,
90
94
  };
91
95
  }
@@ -99,7 +103,7 @@ export function useNcpChatSnapshotSync(params: {
99
103
  lastSendError: string | null;
100
104
  isSending: boolean;
101
105
  modelOptions: ChatModelOption[];
102
- sessionTypeOptions: Array<{ value: string; label: string }>;
106
+ sessionTypeOptions: ChatSessionTypeOption[];
103
107
  selectedSessionType: string;
104
108
  canEditSessionType: boolean;
105
109
  sessionTypeUnavailable: boolean;
@@ -107,6 +111,7 @@ export function useNcpChatSnapshotSync(params: {
107
111
  isSkillsLoading: boolean;
108
112
  sessionTypeUnavailableMessage: string | null;
109
113
  currentSessionTypeLabel: string;
114
+ currentSessionTypeIcon: ChatSessionTypeOption['icon'];
110
115
  sessionKey: string;
111
116
  currentAgentId: string;
112
117
  currentAgent: AgentProfileView | null;
@@ -145,6 +150,7 @@ export function useNcpChatSnapshotSync(params: {
145
150
  sessionTypeUnavailable: params.sessionTypeUnavailable,
146
151
  sessionTypeUnavailableMessage: params.sessionTypeUnavailableMessage,
147
152
  sessionTypeLabel: params.currentSessionTypeLabel,
153
+ sessionTypeIcon: params.currentSessionTypeIcon,
148
154
  sessionKey: params.sessionKey,
149
155
  agentId: params.currentAgentId,
150
156
  agentDisplayName: params.currentAgent?.displayName ?? null,
@@ -1,7 +1,7 @@
1
1
  import { create } from 'zustand';
2
2
  import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
3
3
  import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
4
- import type { SessionSkillEntryView, ThinkingLevel } from '@/api/types';
4
+ import type { SessionSkillEntryView, SessionTypeIconView, ThinkingLevel } from '@/api/types';
5
5
  import type { ChatModelOption } from '@/components/chat/chat-input.types';
6
6
  import { createInitialChatComposerNodes } from '@/components/chat/chat-composer-state';
7
7
 
@@ -24,6 +24,7 @@ export type ChatInputSnapshot = {
24
24
  sessionTypeOptions: Array<{
25
25
  value: string;
26
26
  label: string;
27
+ icon?: SessionTypeIconView | null;
27
28
  ready?: boolean;
28
29
  reason?: string | null;
29
30
  reasonMessage?: string | null;
@@ -3,7 +3,7 @@ import type { MutableRefObject } from 'react';
3
3
  import type { NcpMessage } from '@nextclaw/ncp';
4
4
  import type { ChatFileOperationLineViewModel } from '@nextclaw/agent-chat-ui';
5
5
  import type { ChatModelOption } from '@/components/chat/chat-input.types';
6
- import type { AgentProfileView } from '@/api/types';
6
+ import type { AgentProfileView, SessionTypeIconView } from '@/api/types';
7
7
 
8
8
  export type ChatChildSessionTab = {
9
9
  sessionKey: string;
@@ -35,6 +35,7 @@ export type ChatThreadSnapshot = {
35
35
  sessionTypeUnavailable: boolean;
36
36
  sessionTypeUnavailableMessage?: string | null;
37
37
  sessionTypeLabel?: string | null;
38
+ sessionTypeIcon?: SessionTypeIconView | null;
38
39
  sessionKey: string | null;
39
40
  agentId?: string | null;
40
41
  agentDisplayName?: string | null;
@@ -70,6 +71,7 @@ const initialSnapshot: ChatThreadSnapshot = {
70
71
  sessionTypeUnavailable: false,
71
72
  sessionTypeUnavailableMessage: null,
72
73
  sessionTypeLabel: null,
74
+ sessionTypeIcon: null,
73
75
  sessionKey: null,
74
76
  agentId: null,
75
77
  agentDisplayName: null,
@@ -1,6 +1,11 @@
1
1
  import { useEffect, useMemo, useRef } from 'react';
2
2
  import type { Dispatch, SetStateAction } from 'react';
3
- import type { AgentProfileView, ChatSessionTypeOptionView, SessionEntryView } from '@/api/types';
3
+ import type {
4
+ AgentProfileView,
5
+ ChatSessionTypeOptionView,
6
+ SessionEntryView,
7
+ SessionTypeIconView
8
+ } from '@/api/types';
4
9
  import { t } from '@/lib/i18n';
5
10
 
6
11
  export const DEFAULT_SESSION_TYPE = 'native';
@@ -8,6 +13,7 @@ export const DEFAULT_SESSION_TYPE = 'native';
8
13
  export type ChatSessionTypeOption = {
9
14
  value: string;
10
15
  label: string;
16
+ icon: SessionTypeIconView | null;
11
17
  ready: boolean;
12
18
  reason?: string | null;
13
19
  reasonMessage?: string | null;
@@ -71,6 +77,7 @@ export function buildSessionTypeOptions(
71
77
  deduped.set(value, {
72
78
  value,
73
79
  label: option.label?.trim() || resolveSessionTypeLabel(value),
80
+ icon: option.icon ?? null,
74
81
  ready: option.ready ?? true,
75
82
  reason: option.reason ?? null,
76
83
  reasonMessage: option.reasonMessage ?? null,
@@ -83,6 +90,7 @@ export function buildSessionTypeOptions(
83
90
  deduped.set(DEFAULT_SESSION_TYPE, {
84
91
  value: DEFAULT_SESSION_TYPE,
85
92
  label: resolveSessionTypeLabel(DEFAULT_SESSION_TYPE),
93
+ icon: null,
86
94
  ready: true,
87
95
  reason: null,
88
96
  reasonMessage: null,
@@ -129,6 +137,7 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
129
137
  options.push({
130
138
  value: currentSessionType,
131
139
  label: resolveSessionTypeLabel(currentSessionType),
140
+ icon: null,
132
141
  ready: true,
133
142
  reason: null,
134
143
  reasonMessage: null,
@@ -0,0 +1,86 @@
1
+ import { Fragment } from "react";
2
+ import { ChevronRight, FileCode2, FolderTree } from "lucide-react";
3
+ import type { WorkspaceFileBreadcrumbViewModel } from "@/lib/session-project/workspace-file-breadcrumb";
4
+ import { t } from "@/lib/i18n";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ function WorkspaceBreadcrumbMetaChip({
8
+ tone = "neutral",
9
+ value,
10
+ }: {
11
+ tone?: "neutral" | "warning";
12
+ value: string;
13
+ }) {
14
+ return (
15
+ <span
16
+ className={cn(
17
+ "inline-flex h-5 items-center rounded-sm border px-1.5 text-[10px] font-medium leading-none",
18
+ tone === "warning"
19
+ ? "border-amber-200 bg-amber-50 text-amber-700"
20
+ : "border-gray-200 bg-gray-50 text-gray-500",
21
+ )}
22
+ >
23
+ {value}
24
+ </span>
25
+ );
26
+ }
27
+
28
+ export function ChatSessionWorkspaceFileBreadcrumbs({
29
+ breadcrumb,
30
+ }: {
31
+ breadcrumb: WorkspaceFileBreadcrumbViewModel;
32
+ }) {
33
+ return (
34
+ <div
35
+ data-testid="workspace-file-breadcrumbs"
36
+ title={breadcrumb.fullPath}
37
+ className="workspace-horizontal-scrollbar overflow-x-auto overflow-y-hidden border-b border-gray-200/80 bg-gray-50/55"
38
+ >
39
+ <div
40
+ data-testid="workspace-file-breadcrumb-scroll"
41
+ className="flex min-w-max items-center gap-2.5 px-3 py-1.5"
42
+ >
43
+ <div className="flex min-w-0 flex-1 items-center gap-1 pr-1">
44
+ {breadcrumb.segments.map((segment, index) => (
45
+ <Fragment key={segment.key}>
46
+ <span
47
+ className={cn(
48
+ "inline-flex h-5 items-center gap-1 rounded-sm px-1 text-[11px] leading-none",
49
+ segment.kind === "workspace"
50
+ ? "bg-primary/8 text-primary"
51
+ : segment.isCurrent
52
+ ? "bg-gray-200/70 text-gray-900"
53
+ : "text-gray-500",
54
+ )}
55
+ >
56
+ {segment.kind === "workspace" ? (
57
+ <FolderTree className="h-3 w-3 shrink-0" />
58
+ ) : segment.isCurrent ? (
59
+ <FileCode2 className="h-3 w-3 shrink-0" />
60
+ ) : null}
61
+ <span>{segment.label}</span>
62
+ </span>
63
+ {index < breadcrumb.segments.length - 1 ? (
64
+ <ChevronRight className="h-3 w-3 shrink-0 text-gray-300" />
65
+ ) : null}
66
+ </Fragment>
67
+ ))}
68
+ </div>
69
+
70
+ {breadcrumb.locationLabel || breadcrumb.truncated ? (
71
+ <div className="flex shrink-0 flex-wrap items-center justify-end gap-1">
72
+ {breadcrumb.locationLabel ? (
73
+ <WorkspaceBreadcrumbMetaChip value={breadcrumb.locationLabel} />
74
+ ) : null}
75
+ {breadcrumb.truncated ? (
76
+ <WorkspaceBreadcrumbMetaChip
77
+ tone="warning"
78
+ value={t("chatWorkspacePreviewTruncated")}
79
+ />
80
+ ) : null}
81
+ </div>
82
+ ) : null}
83
+ </div>
84
+ </div>
85
+ );
86
+ }
@@ -1,5 +1,6 @@
1
1
  import { useAppMeta } from '@/hooks/useConfig';
2
2
  import type { ReactNode } from 'react';
3
+ import { RuntimeStatusEntry } from '@/components/layout/runtime-status-entry';
3
4
 
4
5
  type BrandHeaderProps = {
5
6
  className?: string;
@@ -10,6 +11,7 @@ export function BrandHeader({ className, suffix }: BrandHeaderProps) {
10
11
  const { data } = useAppMeta();
11
12
  const productName = data?.name ?? 'NextClaw';
12
13
  const productVersion = data?.productVersion?.trim();
14
+ const resolvedSuffix = suffix ?? <RuntimeStatusEntry />;
13
15
 
14
16
  return (
15
17
  <div className={className ?? 'flex items-center gap-2.5'}>
@@ -19,7 +21,7 @@ export function BrandHeader({ className, suffix }: BrandHeaderProps) {
19
21
  <div className="flex items-baseline gap-2 min-w-0">
20
22
  <span className="truncate text-[15px] font-semibold tracking-[-0.01em] text-gray-800">{productName}</span>
21
23
  {productVersion ? <span className="text-[13px] font-medium text-gray-500">v{productVersion}</span> : null}
22
- {suffix ? <span className="inline-flex items-center shrink-0">{suffix}</span> : null}
24
+ {resolvedSuffix ? <span className="inline-flex items-center shrink-0">{resolvedSuffix}</span> : null}
23
25
  </div>
24
26
  </div>
25
27
  );
@@ -1,4 +1,5 @@
1
1
  import { type SessionContextIcon } from '@/lib/session-context.utils';
2
+ import { resolveAppResourceUri } from '@/lib/app-resource-uri';
2
3
  import { LogoBadge } from '@/components/common/LogoBadge';
3
4
  import { getChannelLogo } from '@/lib/logos';
4
5
  import { cn } from '@/lib/utils';
@@ -8,6 +9,18 @@ export function SessionContextIconNode({ icon, className }: { icon: SessionConte
8
9
  if (icon.kind === 'channel-logo') {
9
10
  return <ChannelLogoIcon channel={icon.channel} className={className} />;
10
11
  }
12
+ if (icon.kind === 'runtime-image') {
13
+ const runtimeIconSrc = resolveAppResourceUri(icon.src);
14
+ return (
15
+ <LogoBadge
16
+ name={icon.name?.trim() || icon.alt?.trim() || 'runtime'}
17
+ src={runtimeIconSrc ?? undefined}
18
+ className={cn('h-[1.125rem] w-[1.125rem]', className)}
19
+ imgClassName="h-full w-full object-contain"
20
+ fallback={<Bot className={cn('h-3 w-3 text-gray-500', className)} />}
21
+ />
22
+ );
23
+ }
11
24
  if (icon.icon === 'heartbeat') {
12
25
  return <HeartPulse className={cn('h-3.5 w-3.5', className)} />;
13
26
  }
@@ -22,8 +35,8 @@ function ChannelLogoIcon(
22
35
  <LogoBadge
23
36
  name={channel}
24
37
  src={logoSrc}
25
- className={cn('h-4 w-4 rounded-[4px] border border-gray-200/80 bg-white', className)}
26
- imgClassName="h-3 w-3 object-contain"
38
+ className={cn('h-[1.125rem] w-[1.125rem]', className)}
39
+ imgClassName="h-full w-full object-contain"
27
40
  fallback={<Bot className="h-3 w-3 text-gray-500" />}
28
41
  />
29
42
  );
@@ -1,7 +1,8 @@
1
- import { useState } from 'react';
2
- import { X } from 'lucide-react';
3
- import { cn } from '@/lib/utils';
4
- import { t } from '@/lib/i18n';
1
+ import { useState } from "react";
2
+ import { X } from "lucide-react";
3
+ import { cn } from "@/lib/utils";
4
+ import { t } from "@/lib/i18n";
5
+ import { TagChip } from "@/components/ui/tag-chip";
5
6
 
6
7
  interface TagInputProps {
7
8
  value: string[];
@@ -10,15 +11,20 @@ interface TagInputProps {
10
11
  placeholder?: string;
11
12
  }
12
13
 
13
- export function TagInput({ value, onChange, className, placeholder = '' }: TagInputProps) {
14
- const [input, setInput] = useState('');
14
+ export function TagInput({
15
+ value,
16
+ onChange,
17
+ className,
18
+ placeholder = "",
19
+ }: TagInputProps) {
20
+ const [input, setInput] = useState("");
15
21
 
16
22
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
17
- if (e.key === 'Enter' && input.trim()) {
23
+ if (e.key === "Enter" && input.trim()) {
18
24
  e.preventDefault();
19
25
  onChange([...value, input.trim()]);
20
- setInput('');
21
- } else if (e.key === 'Backspace' && !input && value.length > 0) {
26
+ setInput("");
27
+ } else if (e.key === "Backspace" && !input && value.length > 0) {
22
28
  onChange(value.slice(0, -1));
23
29
  }
24
30
  };
@@ -28,21 +34,23 @@ export function TagInput({ value, onChange, className, placeholder = '' }: TagIn
28
34
  };
29
35
 
30
36
  return (
31
- <div className={cn('flex flex-wrap gap-2 p-2 border rounded-md min-h-[42px]', className)}>
37
+ <div
38
+ className={cn(
39
+ "flex min-h-[42px] flex-wrap gap-2 rounded-xl border border-gray-200/80 bg-white p-2",
40
+ className,
41
+ )}
42
+ >
32
43
  {value.map((tag, index) => (
33
- <span
34
- key={index}
35
- className="inline-flex items-center gap-1 px-2 py-1 bg-primary text-primary-foreground rounded text-sm"
36
- >
44
+ <TagChip key={index} tone="info" className="gap-1 px-2 py-1 text-sm">
37
45
  {tag}
38
46
  <button
39
47
  type="button"
40
48
  onClick={() => removeTag(index)}
41
- className="hover:text-red-300 transition-colors"
49
+ className="transition-colors hover:text-rose-200"
42
50
  >
43
51
  <X className="h-3 w-3" />
44
52
  </button>
45
- </span>
53
+ </TagChip>
46
54
  ))}
47
55
  <input
48
56
  type="text"
@@ -50,7 +58,7 @@ export function TagInput({ value, onChange, className, placeholder = '' }: TagIn
50
58
  onChange={(e) => setInput(e.target.value)}
51
59
  onKeyDown={handleKeyDown}
52
60
  className="flex-1 outline-none min-w-[100px] bg-transparent text-sm"
53
- placeholder={placeholder || t('enterTag')}
61
+ placeholder={placeholder || t("enterTag")}
54
62
  />
55
63
  </div>
56
64
  );