@nextclaw/ui 0.14.1-beta.0 → 0.14.1

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 (144) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/assets/{api-C6EDHBDR.js → api-DPcvCLfa.js} +3 -3
  3. package/dist/assets/{channels-list-page-DKj6m1Hc.js → channels-list-page-D6jtziu4.js} +2 -2
  4. package/dist/assets/chat-page-BBnNJoKG.js +105 -0
  5. package/dist/assets/{config-split-page-CjlmJy8X.js → config-split-page-BCJhqdK9.js} +1 -1
  6. package/dist/assets/{confirm-dialog-B39X0pYp.js → confirm-dialog-CWh5FfK2.js} +1 -1
  7. package/dist/assets/{desktop-update-config-DfUQ8nly.js → desktop-update-config-DsRhBizi.js} +1 -1
  8. package/dist/assets/{dist-CmMro3nV.js → dist-DPrgcEC0.js} +1 -1
  9. package/dist/assets/{dist-BIYV2-Ia.js → dist-DsE4OsSS.js} +1 -1
  10. package/dist/assets/doc-browser-C8xOF_9F.js +1 -0
  11. package/dist/assets/{doc-browser-CDtUtcim.js → doc-browser-DgQedeAn.js} +1 -1
  12. package/dist/assets/{doc-browser-context-DZbC9zVB.js → doc-browser-context-B46tQGmO.js} +1 -1
  13. package/dist/assets/{ellipsis-DDpgTzgx.js → ellipsis-B81FStLm.js} +1 -1
  14. package/dist/assets/{external-link-D2cBOCqn.js → external-link-C_dmG-WJ.js} +1 -1
  15. package/dist/assets/index-7o1WdUbU.js +103 -0
  16. package/dist/assets/index-DQWmEWzk.css +1 -0
  17. package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +1 -0
  18. package/dist/assets/mcp-marketplace-page-Dzull_4-.js +40 -0
  19. package/dist/assets/model-config-page-Cv0O3BIj.js +1 -0
  20. package/dist/assets/plus-Bnt56oqG.js +1 -0
  21. package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +1 -0
  22. package/dist/assets/providers-config-page-CPDz0Lxc.js +1 -0
  23. package/dist/assets/{react-DYQXXGbH.js → react-sZz5HSCm.js} +1 -1
  24. package/dist/assets/remote-B0LMs8u7.js +1 -0
  25. package/dist/assets/rotate-cw-BfyG_KEF.js +1 -0
  26. package/dist/assets/runtime-config-page-q0fvv8ME.js +1 -0
  27. package/dist/assets/{save-BNVTgBVn.js → save-MzjoiHri.js} +1 -1
  28. package/dist/assets/{search-CKCzwLId.js → search-B29dE4GN.js} +1 -1
  29. package/dist/assets/{search-config-page-DG5Q2hF1.js → search-config-page-Dl5sZzob.js} +1 -1
  30. package/dist/assets/secrets-config-page-C_l-IFCK.js +3 -0
  31. package/dist/assets/{select-CIRwTNP4.js → select-BKmh05hd.js} +2 -2
  32. package/dist/assets/{tag-chip-DKhVFtTE.js → tag-chip-BS_7Ueom.js} +1 -1
  33. package/dist/assets/use-config-DZkBWIdL.js +1 -0
  34. package/dist/assets/x-CW2KO-Hz.js +1 -0
  35. package/dist/index.html +14 -14
  36. package/package.json +9 -9
  37. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +12 -63
  38. package/src/features/chat/components/conversation/chat-conversation-content.tsx +11 -50
  39. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +15 -2
  40. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +70 -7
  41. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +104 -16
  42. package/src/features/chat/components/layout/chat-sidebar.tsx +21 -2
  43. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +84 -6
  44. package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +2 -0
  45. package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +88 -0
  46. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +64 -24
  47. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +1 -0
  48. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +21 -12
  49. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +80 -25
  50. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +310 -0
  51. package/src/features/chat/features/message/utils/__tests__/chat-message-test.utils.ts +43 -0
  52. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +1 -346
  53. package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +10 -9
  54. package/src/features/chat/features/message/utils/file-operation/diff.utils.ts +39 -36
  55. package/src/features/chat/features/message/utils/file-operation/line-builder.utils.ts +46 -38
  56. package/src/features/chat/features/message/utils/file-operation/parsed-block.utils.ts +18 -15
  57. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +3 -2
  58. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +25 -25
  59. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +15 -2
  60. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +24 -0
  61. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +27 -1
  62. package/src/features/chat/features/session-type/components/{chat-sidebar-create-menu.tsx → chat-session-type-menu.tsx} +17 -4
  63. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +15 -3
  64. package/src/features/chat/features/session-type/hooks/__tests__/use-chat-new-session-type-preference.test.tsx +152 -0
  65. package/src/features/chat/features/session-type/hooks/use-chat-new-session-type-preference.ts +159 -0
  66. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +193 -0
  67. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +123 -0
  68. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +104 -0
  69. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +72 -0
  70. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +54 -0
  71. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +132 -0
  72. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +82 -0
  73. package/src/features/chat/features/welcome/components/chat-welcome.tsx +121 -0
  74. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +53 -0
  75. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-selection.utils.test.ts +63 -0
  76. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-visibility.utils.test.ts +34 -0
  77. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +71 -0
  78. package/src/features/chat/features/welcome/utils/chat-welcome-selection.utils.ts +79 -0
  79. package/src/features/chat/features/welcome/utils/chat-welcome-visibility.utils.ts +17 -0
  80. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +65 -0
  81. package/src/features/chat/managers/chat-input.manager.ts +24 -2
  82. package/src/features/chat/stores/chat-input.store.ts +2 -0
  83. package/src/features/chat/types/chat-input-bar.types.ts +5 -0
  84. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +24 -0
  85. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +8 -3
  86. package/src/features/marketplace/components/marketplace-item-list-view.tsx +2 -2
  87. package/src/features/marketplace/components/marketplace-list-card.tsx +124 -98
  88. package/src/features/marketplace/components/marketplace-page-parts.tsx +11 -6
  89. package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +27 -23
  90. package/src/features/settings/components/config/provider-form.tsx +284 -387
  91. package/src/features/settings/components/config/provider-status-badge.tsx +4 -4
  92. package/src/features/settings/components/config/secrets-config-form.tsx +11 -135
  93. package/src/features/settings/hooks/use-provider-auth-flow.ts +140 -0
  94. package/src/features/settings/pages/__tests__/model-config-page.test.tsx +16 -0
  95. package/src/features/settings/pages/secrets-config-page.tsx +2 -1
  96. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +60 -1
  97. package/src/features/settings/utils/__tests__/secrets-config-form.utils.test.ts +66 -0
  98. package/src/features/settings/utils/provider-form-context.utils.ts +131 -0
  99. package/src/features/settings/utils/provider-form-model.utils.ts +101 -0
  100. package/src/features/settings/utils/provider-form-support.utils.ts +102 -1
  101. package/src/features/settings/utils/secrets-config-form.utils.ts +139 -0
  102. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +17 -11
  103. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +14 -9
  104. package/src/features/system-status/components/config/runtime-config-editor.tsx +8 -5
  105. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +14 -9
  106. package/src/features/system-status/components/config/runtime-settings-card.tsx +16 -7
  107. package/src/features/system-status/components/runtime-security-card.tsx +79 -25
  108. package/src/shared/components/common/__tests__/agent-avatar.test.tsx +3 -1
  109. package/src/shared/components/common/__tests__/searchable-model-input.test.tsx +34 -0
  110. package/src/shared/components/common/agent-avatar.tsx +5 -2
  111. package/src/shared/components/common/provider-scoped-model-input.tsx +1 -0
  112. package/src/shared/components/common/searchable-model-input.tsx +102 -87
  113. package/src/shared/components/ui/README.md +7 -0
  114. package/src/shared/components/ui/popover.tsx +19 -3
  115. package/src/shared/components/ui/select.tsx +14 -4
  116. package/src/shared/hooks/use-infinite-scroll-loader.ts +14 -10
  117. package/src/shared/lib/api/index.ts +3 -0
  118. package/src/shared/lib/api/preferences/preference-keys.config.ts +9 -0
  119. package/src/shared/lib/api/preferences/preference.types.ts +22 -0
  120. package/src/shared/lib/api/preferences/preference.utils.ts +40 -0
  121. package/src/shared/lib/i18n/locales/en-US/chat.json +16 -1
  122. package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
  123. package/src/shared/lib/i18n/locales/en-US/marketplace.json +2 -0
  124. package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -1
  125. package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
  126. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +2 -0
  127. package/dist/assets/chat-page-BXngLJY6.js +0 -105
  128. package/dist/assets/doc-browser-B6Wc_TIY.js +0 -1
  129. package/dist/assets/index-DTb_XVIH.js +0 -103
  130. package/dist/assets/index-ayiPYhf1.css +0 -1
  131. package/dist/assets/mcp-marketplace-page-DZ9H0v05.js +0 -1
  132. package/dist/assets/mcp-marketplace-page-cnVWf7QQ.js +0 -40
  133. package/dist/assets/model-config-page-Bcb35AVv.js +0 -1
  134. package/dist/assets/plus-CYE1onU2.js +0 -1
  135. package/dist/assets/provider-scoped-model-input-kdoNp4HG.js +0 -1
  136. package/dist/assets/providers-config-page-CQSibSwO.js +0 -1
  137. package/dist/assets/remote-BlJDbXOI.js +0 -1
  138. package/dist/assets/rotate-cw-BWbvFDAX.js +0 -1
  139. package/dist/assets/runtime-config-page-Tn2yLQ0c.js +0 -1
  140. package/dist/assets/secrets-config-page-BXn5Uuwp.js +0 -3
  141. package/dist/assets/use-config-DgKM9VIU.js +0 -1
  142. package/dist/assets/x-Cm3JjYut.js +0 -1
  143. package/src/features/chat/components/__tests__/chat-welcome.test.tsx +0 -45
  144. package/src/features/chat/components/chat-welcome.tsx +0 -102
@@ -0,0 +1,101 @@
1
+ import type { ThinkingLevel } from '@/shared/lib/api';
2
+ import {
3
+ THINKING_LEVELS,
4
+ toProviderLocalModelId,
5
+ type ModelConfig
6
+ } from './provider-form-support.utils';
7
+
8
+ type AddProviderModelResult = {
9
+ models: string[];
10
+ draft: string;
11
+ errorKey?: 'providerModelInvalidProviderPrefix';
12
+ };
13
+
14
+ export function addProviderLocalModel(models: string[], draft: string, aliases: string[]): AddProviderModelResult {
15
+ const next = toProviderLocalModelId(draft, aliases);
16
+ if (!next) {
17
+ return { models, draft };
18
+ }
19
+ if (next.includes('/')) {
20
+ return { models, draft, errorKey: 'providerModelInvalidProviderPrefix' };
21
+ }
22
+ if (models.includes(next)) {
23
+ return { models, draft: '' };
24
+ }
25
+ return { models: [...models, next], draft: '' };
26
+ }
27
+
28
+ export function removeProviderLocalModel(models: string[], modelConfig: ModelConfig, modelName: string): {
29
+ models: string[];
30
+ modelConfig: ModelConfig;
31
+ } {
32
+ const nextConfig = { ...modelConfig };
33
+ delete nextConfig[modelName];
34
+ return {
35
+ models: models.filter((name) => name !== modelName),
36
+ modelConfig: nextConfig
37
+ };
38
+ }
39
+
40
+ export function toggleModelThinkingLevelInConfig(modelConfig: ModelConfig, modelName: string, level: ThinkingLevel): ModelConfig {
41
+ const currentEntry = modelConfig[modelName];
42
+ const currentLevels = currentEntry?.thinking?.supported ?? [];
43
+ const nextLevels = currentLevels.includes(level)
44
+ ? currentLevels.filter((item) => item !== level)
45
+ : THINKING_LEVELS.filter((item) => item === level || currentLevels.includes(item));
46
+ const nextDefault =
47
+ currentEntry?.thinking?.default && nextLevels.includes(currentEntry.thinking.default)
48
+ ? currentEntry.thinking.default
49
+ : undefined;
50
+ const nextEntry = {
51
+ ...currentEntry,
52
+ thinking:
53
+ nextLevels.length > 0
54
+ ? nextDefault
55
+ ? { supported: nextLevels, default: nextDefault }
56
+ : { supported: nextLevels }
57
+ : undefined
58
+ };
59
+ const next = { ...modelConfig };
60
+ if (nextEntry.thinking || nextEntry.vision === true) {
61
+ next[modelName] = nextEntry;
62
+ } else {
63
+ delete next[modelName];
64
+ }
65
+ return next;
66
+ }
67
+
68
+ export function setModelThinkingDefaultInConfig(modelConfig: ModelConfig, modelName: string, level: ThinkingLevel | null): ModelConfig {
69
+ const currentEntry = modelConfig[modelName];
70
+ const currentThinking = currentEntry?.thinking;
71
+ if (!currentThinking || currentThinking.supported.length === 0) {
72
+ return modelConfig;
73
+ }
74
+ if (level && !currentThinking.supported.includes(level)) {
75
+ return modelConfig;
76
+ }
77
+ return {
78
+ ...modelConfig,
79
+ [modelName]: {
80
+ ...currentEntry,
81
+ thinking: level
82
+ ? { supported: currentThinking.supported, default: level }
83
+ : { supported: currentThinking.supported }
84
+ }
85
+ };
86
+ }
87
+
88
+ export function setModelVisionInConfig(modelConfig: ModelConfig, modelName: string, vision: boolean): ModelConfig {
89
+ const currentEntry = modelConfig[modelName];
90
+ const nextEntry = {
91
+ ...currentEntry,
92
+ vision: vision ? true : undefined
93
+ };
94
+ const next = { ...modelConfig };
95
+ if (nextEntry.thinking || nextEntry.vision === true) {
96
+ next[modelName] = nextEntry;
97
+ } else {
98
+ delete next[modelName];
99
+ }
100
+ return next;
101
+ }
@@ -1,6 +1,11 @@
1
1
  import { t } from '@/shared/lib/i18n';
2
2
  import type { getLanguage } from '@/shared/lib/i18n';
3
- import type { ProviderConfigView, ThinkingLevel } from '@/shared/lib/api';
3
+ import type {
4
+ ProviderConfigUpdate,
5
+ ProviderConfigView,
6
+ ProviderConnectionTestRequest,
7
+ ThinkingLevel
8
+ } from '@/shared/lib/api';
4
9
  import {
5
10
  normalizeModelConfigMap,
6
11
  normalizeThinkingLevels,
@@ -14,6 +19,35 @@ type ModelConfig = Record<string, ModelConfigEntry>;
14
19
  type ProviderAuthMethodOption = {
15
20
  id: string;
16
21
  };
22
+ type ProviderFormChangeInput = {
23
+ providerName?: string;
24
+ apiKey: string;
25
+ apiBase: string;
26
+ currentApiBase: string;
27
+ extraHeaders: Record<string, string> | null;
28
+ currentHeaders: Record<string, string> | null;
29
+ supportsWireApi: boolean;
30
+ wireApi: WireApiType;
31
+ currentWireApi: WireApiType;
32
+ models: string[];
33
+ currentEditableModels: string[];
34
+ modelConfig: ModelConfig;
35
+ currentModelConfig: ModelConfig;
36
+ providerDisplayName: string;
37
+ effectiveDisplayName: string;
38
+ };
39
+ type ProviderSavePayloadInput = ProviderFormChangeInput & {
40
+ defaultApiBase: string;
41
+ };
42
+ type ProviderConnectionTestPayloadInput = {
43
+ apiKey: string;
44
+ apiBase: string;
45
+ extraHeaders: Record<string, string> | null;
46
+ supportsWireApi: boolean;
47
+ wireApi: WireApiType;
48
+ models: string[];
49
+ providerModelAliases: string[];
50
+ };
17
51
 
18
52
  const EMPTY_PROVIDER_CONFIG: ProviderConfigView = {
19
53
  providerId: '',
@@ -196,6 +230,70 @@ function modelConfigEqual(left: ModelConfig, right: ModelConfig): boolean {
196
230
  return true;
197
231
  }
198
232
 
233
+ function hasProviderFormChanges(input: ProviderFormChangeInput): boolean {
234
+ if (!input.providerName) {
235
+ return false;
236
+ }
237
+ return (
238
+ input.apiKey.trim().length > 0 ||
239
+ input.apiBase.trim() !== input.currentApiBase.trim() ||
240
+ !headersEqual(input.extraHeaders, input.currentHeaders) ||
241
+ (input.supportsWireApi && input.wireApi !== input.currentWireApi) ||
242
+ !modelListsEqual(input.models, input.currentEditableModels) ||
243
+ !modelConfigEqual(input.modelConfig, input.currentModelConfig) ||
244
+ input.providerDisplayName.trim() !== input.effectiveDisplayName
245
+ );
246
+ }
247
+
248
+ function buildProviderSavePayload(input: ProviderSavePayloadInput): ProviderConfigUpdate {
249
+ const payload: ProviderConfigUpdate = {};
250
+ const trimmedApiKey = input.apiKey.trim();
251
+ const trimmedApiBase = input.apiBase.trim();
252
+ const normalizedHeaders = normalizeHeaders(input.extraHeaders);
253
+ const trimmedDisplayName = input.providerDisplayName.trim();
254
+
255
+ if (trimmedDisplayName !== input.effectiveDisplayName) {
256
+ payload.displayName = trimmedDisplayName.length > 0 ? trimmedDisplayName : null;
257
+ }
258
+ if (trimmedApiKey.length > 0) {
259
+ payload.apiKey = trimmedApiKey;
260
+ }
261
+ if (trimmedApiBase !== input.currentApiBase.trim()) {
262
+ payload.apiBase = trimmedApiBase.length > 0 && trimmedApiBase !== input.defaultApiBase ? trimmedApiBase : null;
263
+ }
264
+ if (!headersEqual(normalizedHeaders, input.currentHeaders)) {
265
+ payload.extraHeaders = normalizedHeaders;
266
+ }
267
+ if (input.supportsWireApi && input.wireApi !== input.currentWireApi) {
268
+ payload.wireApi = input.wireApi;
269
+ }
270
+ if (!modelListsEqual(input.models, input.currentEditableModels)) {
271
+ payload.models = serializeModelsForSave(input.models, input.providerName ?? '');
272
+ }
273
+ if (!modelConfigEqual(input.modelConfig, input.currentModelConfig)) {
274
+ payload.modelConfig = normalizeModelConfigForModels(input.modelConfig, input.models);
275
+ }
276
+
277
+ return payload;
278
+ }
279
+
280
+ function buildProviderConnectionTestPayload(input: ProviderConnectionTestPayloadInput): ProviderConnectionTestRequest {
281
+ const preferredModel = input.models.find((modelName) => modelName.trim().length > 0) ?? '';
282
+ const testModel = toProviderLocalModelId(preferredModel, input.providerModelAliases);
283
+ const payload: ProviderConnectionTestRequest = {
284
+ apiBase: input.apiBase.trim(),
285
+ extraHeaders: normalizeHeaders(input.extraHeaders),
286
+ model: testModel || null
287
+ };
288
+ if (input.apiKey.trim().length > 0) {
289
+ payload.apiKey = input.apiKey.trim();
290
+ }
291
+ if (input.supportsWireApi) {
292
+ payload.wireApi = input.wireApi;
293
+ }
294
+ return payload;
295
+ }
296
+
199
297
  function formatThinkingLevelLabel(level: ThinkingLevel): string {
200
298
  if (level === 'off') {
201
299
  return t('chatThinkingLevelOff');
@@ -285,8 +383,11 @@ function shouldUsePillSelector(params: {
285
383
 
286
384
  export type { ModelConfig, ProviderAuthMethodOption, WireApiType };
287
385
  export {
386
+ buildProviderConnectionTestPayload,
387
+ buildProviderSavePayload,
288
388
  EMPTY_PROVIDER_CONFIG,
289
389
  formatThinkingLevelLabel,
390
+ hasProviderFormChanges,
290
391
  headersEqual,
291
392
  modelListsEqual,
292
393
  modelConfigEqual,
@@ -0,0 +1,139 @@
1
+ import type { SecretProviderView, SecretRefView, SecretSourceView, SecretsView } from '@/shared/lib/api';
2
+ import { t } from '@/shared/lib/i18n';
3
+
4
+ export type ProviderRow = {
5
+ alias: string;
6
+ source: SecretSourceView;
7
+ prefix: string;
8
+ path: string;
9
+ command: string;
10
+ argsText: string;
11
+ cwd: string;
12
+ timeoutMs: number;
13
+ };
14
+
15
+ export type RefRow = {
16
+ path: string;
17
+ source: SecretSourceView;
18
+ provider: string;
19
+ id: string;
20
+ };
21
+
22
+ export type SecretsFormState = {
23
+ enabled: boolean;
24
+ defaultEnv: string;
25
+ defaultFile: string;
26
+ defaultExec: string;
27
+ providers: ProviderRow[];
28
+ refs: RefRow[];
29
+ };
30
+
31
+ export const SOURCE_OPTIONS: SecretSourceView[] = ['env', 'file', 'exec'];
32
+
33
+ export function createProviderRow(alias = ''): ProviderRow {
34
+ return { alias, source: 'env', prefix: '', path: '', command: '', argsText: '', cwd: '', timeoutMs: 5000 };
35
+ }
36
+
37
+ export function createRefRow(): RefRow {
38
+ return { path: '', source: 'env', provider: '', id: '' };
39
+ }
40
+
41
+ function providerToRow(alias: string, provider: SecretProviderView): ProviderRow {
42
+ if (provider.source === 'env') return { ...createProviderRow(alias), source: 'env', prefix: provider.prefix ?? '' };
43
+ if (provider.source === 'file') return { ...createProviderRow(alias), source: 'file', path: provider.path };
44
+ return {
45
+ ...createProviderRow(alias),
46
+ source: 'exec',
47
+ command: provider.command,
48
+ argsText: (provider.args ?? []).join('\n'),
49
+ cwd: provider.cwd ?? '',
50
+ timeoutMs: provider.timeoutMs ?? 5000,
51
+ };
52
+ }
53
+
54
+ function rowToProvider(row: ProviderRow): SecretProviderView {
55
+ if (row.source === 'env') return { source: 'env', ...(row.prefix.trim() ? { prefix: row.prefix.trim() } : {}) };
56
+ if (row.source === 'file') return { source: 'file', path: row.path.trim(), format: 'json' };
57
+ return {
58
+ source: 'exec',
59
+ command: row.command.trim(),
60
+ args: row.argsText.split('\n').map((item) => item.trim()).filter(Boolean),
61
+ ...(row.cwd.trim() ? { cwd: row.cwd.trim() } : {}),
62
+ timeoutMs: Math.max(1, Math.trunc(row.timeoutMs || 5000)),
63
+ };
64
+ }
65
+
66
+ export function buildSecretsFormState(secrets?: SecretsView): SecretsFormState {
67
+ if (!secrets) {
68
+ return { enabled: true, defaultEnv: '', defaultFile: '', defaultExec: '', providers: [], refs: [] };
69
+ }
70
+ return {
71
+ enabled: Boolean(secrets.enabled),
72
+ defaultEnv: secrets.defaults.env ?? '',
73
+ defaultFile: secrets.defaults.file ?? '',
74
+ defaultExec: secrets.defaults.exec ?? '',
75
+ providers: Object.entries(secrets.providers).map(([alias, provider]) => providerToRow(alias, provider)),
76
+ refs: Object.entries(secrets.refs).map(([path, ref]) => ({
77
+ path,
78
+ source: ref.source,
79
+ provider: ref.provider ?? '',
80
+ id: ref.id,
81
+ })),
82
+ };
83
+ }
84
+
85
+ export function prepareSecretsFormSubmitState(state: SecretsFormState): SecretsFormState {
86
+ const providerMap: Record<string, SecretProviderView> = {};
87
+ for (const [index, row] of state.providers.entries()) {
88
+ const alias = row.alias.trim();
89
+ if (!alias) throw new Error(`${t('providerAlias')} #${index + 1} ${t('isRequired')}`);
90
+ if (providerMap[alias]) throw new Error(`${t('providerAlias')}: ${alias} (${t('duplicate')})`);
91
+ if (row.source === 'file' && !row.path.trim()) throw new Error(`${t('secretFilePath')} #${index + 1} ${t('isRequired')}`);
92
+ if (row.source === 'exec' && !row.command.trim()) throw new Error(`${t('secretExecCommand')} #${index + 1} ${t('isRequired')}`);
93
+ providerMap[alias] = rowToProvider(row);
94
+ }
95
+
96
+ const refMap: Record<string, SecretRefView> = {};
97
+ for (const [index, row] of state.refs.entries()) {
98
+ const path = row.path.trim();
99
+ const id = row.id.trim();
100
+ if (!path) throw new Error(`${t('secretConfigPath')} #${index + 1} ${t('isRequired')}`);
101
+ if (!id) throw new Error(`${t('secretId')} #${index + 1} ${t('isRequired')}`);
102
+ const provider = row.provider.trim();
103
+ if (provider && !providerMap[provider]) throw new Error(`${t('secretProviderAlias')}: ${provider} ${t('notFound')}`);
104
+ refMap[path] = { source: row.source, ...(provider ? { provider } : {}), id };
105
+ }
106
+
107
+ return {
108
+ enabled: state.enabled,
109
+ defaultEnv: state.defaultEnv.trim(),
110
+ defaultFile: state.defaultFile.trim(),
111
+ defaultExec: state.defaultExec.trim(),
112
+ providers: Object.entries(providerMap).map(([alias, provider]) => providerToRow(alias, provider)),
113
+ refs: Object.entries(refMap).map(([path, ref]) => ({ path, source: ref.source, provider: ref.provider ?? '', id: ref.id })),
114
+ };
115
+ }
116
+
117
+ export function buildSecretsSubmitPayload(state: SecretsFormState) {
118
+ return {
119
+ enabled: state.enabled,
120
+ defaults: {
121
+ env: state.defaultEnv || null,
122
+ file: state.defaultFile || null,
123
+ exec: state.defaultExec || null,
124
+ },
125
+ providers: Object.fromEntries(
126
+ state.providers.map((row) => [row.alias.trim(), rowToProvider(row)]).filter(([alias]) => alias),
127
+ ),
128
+ refs: Object.fromEntries(
129
+ state.refs.map((row) => [
130
+ row.path.trim(),
131
+ {
132
+ source: row.source,
133
+ ...(row.provider.trim() ? { provider: row.provider.trim() } : {}),
134
+ id: row.id.trim(),
135
+ },
136
+ ]).filter(([path, ref]) => path && (ref as SecretRefView).id),
137
+ ),
138
+ };
139
+ }
@@ -7,7 +7,13 @@ import { Switch } from '@/shared/components/ui/switch';
7
7
  import { t } from '@/shared/lib/i18n';
8
8
  import { parseOptionalInt } from '@/features/system-status/utils/runtime-config-agent.utils';
9
9
 
10
- export function RuntimeAgentListCard(props: {
10
+ export function RuntimeAgentListCard({
11
+ agents,
12
+ onAddAgent,
13
+ onRemoveAgent,
14
+ onSetDefaultAgent,
15
+ onUpdateAgent
16
+ }: {
11
17
  agents: AgentProfileView[];
12
18
  onUpdateAgent: (index: number, patch: Partial<AgentProfileView>) => void;
13
19
  onRemoveAgent: (index: number) => void;
@@ -21,44 +27,44 @@ export function RuntimeAgentListCard(props: {
21
27
  <CardDescription>{t('agentListHelp')}</CardDescription>
22
28
  </CardHeader>
23
29
  <CardContent className="space-y-3">
24
- {props.agents.map((agent, index) => (
30
+ {agents.map((agent, index) => (
25
31
  <div key={`${index}-${agent.id}`} className="rounded-xl border border-gray-200 p-3 space-y-3">
26
32
  <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
27
- <Input value={agent.id} onChange={(event) => props.onUpdateAgent(index, { id: event.target.value })} placeholder={t('agentIdPlaceholder')} />
28
- <Input value={agent.workspace ?? ''} onChange={(event) => props.onUpdateAgent(index, { workspace: event.target.value })} placeholder={t('workspaceOverridePlaceholder')} />
29
- <Input value={agent.model ?? ''} onChange={(event) => props.onUpdateAgent(index, { model: event.target.value })} placeholder={t('modelOverridePlaceholder')} />
30
- <Input value={agent.runtime ?? agent.engine ?? ''} onChange={(event) => props.onUpdateAgent(index, { runtime: event.target.value })} placeholder={t('engineOverridePlaceholder')} />
33
+ <Input value={agent.id} onChange={(event) => onUpdateAgent(index, { id: event.target.value })} placeholder={t('agentIdPlaceholder')} />
34
+ <Input value={agent.workspace ?? ''} onChange={(event) => onUpdateAgent(index, { workspace: event.target.value })} placeholder={t('workspaceOverridePlaceholder')} />
35
+ <Input value={agent.model ?? ''} onChange={(event) => onUpdateAgent(index, { model: event.target.value })} placeholder={t('modelOverridePlaceholder')} />
36
+ <Input value={agent.runtime ?? agent.engine ?? ''} onChange={(event) => onUpdateAgent(index, { runtime: event.target.value })} placeholder={t('engineOverridePlaceholder')} />
31
37
  <div className="grid grid-cols-1 md:grid-cols-2 gap-2">
32
38
  <Input
33
39
  type="number"
34
40
  min={1000}
35
41
  step={1000}
36
42
  value={agent.contextTokens ?? ''}
37
- onChange={(event) => props.onUpdateAgent(index, { contextTokens: parseOptionalInt(event.target.value) })}
43
+ onChange={(event) => onUpdateAgent(index, { contextTokens: parseOptionalInt(event.target.value) })}
38
44
  placeholder={t('contextTokensPlaceholder')}
39
45
  />
40
46
  <Input
41
47
  type="number"
42
48
  min={1}
43
49
  value={agent.maxToolIterations ?? ''}
44
- onChange={(event) => props.onUpdateAgent(index, { maxToolIterations: parseOptionalInt(event.target.value) })}
50
+ onChange={(event) => onUpdateAgent(index, { maxToolIterations: parseOptionalInt(event.target.value) })}
45
51
  placeholder={t('maxToolsPlaceholder')}
46
52
  />
47
53
  </div>
48
54
  </div>
49
55
  <div className="flex items-center justify-between">
50
56
  <div className="flex items-center gap-2 text-sm text-gray-600">
51
- <Switch checked={Boolean(agent.default)} onCheckedChange={(checked) => props.onSetDefaultAgent(index, checked)} />
57
+ <Switch checked={Boolean(agent.default)} onCheckedChange={(checked) => onSetDefaultAgent(index, checked)} />
52
58
  <span>{t('defaultAgent')}</span>
53
59
  </div>
54
- <Button type="button" variant="outline" size="sm" onClick={() => props.onRemoveAgent(index)}>
60
+ <Button type="button" variant="outline" size="sm" onClick={() => onRemoveAgent(index)}>
55
61
  <Trash2 className="h-4 w-4 mr-1" />
56
62
  {t('remove')}
57
63
  </Button>
58
64
  </div>
59
65
  </div>
60
66
  ))}
61
- <Button type="button" variant="outline" onClick={props.onAddAgent}>
67
+ <Button type="button" variant="outline" onClick={onAddAgent}>
62
68
  <Plus className="h-4 w-4 mr-2" />
63
69
  {t('addAgent')}
64
70
  </Button>
@@ -7,7 +7,12 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
7
7
  import { t } from '@/shared/lib/i18n';
8
8
  import type { PeerKind } from '@/features/system-status/utils/runtime-config-agent.utils';
9
9
 
10
- export function RuntimeBindingListCard(props: {
10
+ export function RuntimeBindingListCard({
11
+ bindings,
12
+ onAddBinding,
13
+ onRemoveBinding,
14
+ onUpdateBinding
15
+ }: {
11
16
  bindings: AgentBindingView[];
12
17
  onUpdateBinding: (index: number, next: AgentBindingView) => void;
13
18
  onRemoveBinding: (index: number) => void;
@@ -20,19 +25,19 @@ export function RuntimeBindingListCard(props: {
20
25
  <CardDescription>{t('bindingsHelp')}</CardDescription>
21
26
  </CardHeader>
22
27
  <CardContent className="space-y-3">
23
- {props.bindings.map((binding, index) => {
28
+ {bindings.map((binding, index) => {
24
29
  const peerKind = (binding.match.peer?.kind ?? '') as PeerKind;
25
30
  return (
26
31
  <div key={`${index}-${binding.agentId}`} className="rounded-xl border border-gray-200 p-3 space-y-3">
27
32
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
28
- <Input value={binding.agentId} onChange={(event) => props.onUpdateBinding(index, { ...binding, agentId: event.target.value })} placeholder={t('targetAgentIdPlaceholder')} />
29
- <Input value={binding.match.channel} onChange={(event) => props.onUpdateBinding(index, { ...binding, match: { ...binding.match, channel: event.target.value } })} placeholder={t('channelPlaceholder')} />
30
- <Input value={binding.match.accountId ?? ''} onChange={(event) => props.onUpdateBinding(index, { ...binding, match: { ...binding.match, accountId: event.target.value } })} placeholder={t('accountIdOptionalPlaceholder')} />
33
+ <Input value={binding.agentId} onChange={(event) => onUpdateBinding(index, { ...binding, agentId: event.target.value })} placeholder={t('targetAgentIdPlaceholder')} />
34
+ <Input value={binding.match.channel} onChange={(event) => onUpdateBinding(index, { ...binding, match: { ...binding.match, channel: event.target.value } })} placeholder={t('channelPlaceholder')} />
35
+ <Input value={binding.match.accountId ?? ''} onChange={(event) => onUpdateBinding(index, { ...binding, match: { ...binding.match, accountId: event.target.value } })} placeholder={t('accountIdOptionalPlaceholder')} />
31
36
  <Select
32
37
  value={peerKind || '__none__'}
33
38
  onValueChange={(value) => {
34
39
  const nextKind = value === '__none__' ? '' : (value as PeerKind);
35
- props.onUpdateBinding(
40
+ onUpdateBinding(
36
41
  index,
37
42
  nextKind
38
43
  ? { ...binding, match: { ...binding.match, peer: { kind: nextKind, id: binding.match.peer?.id ?? '' } } }
@@ -53,7 +58,7 @@ export function RuntimeBindingListCard(props: {
53
58
  <Input
54
59
  value={binding.match.peer?.id ?? ''}
55
60
  onChange={(event) =>
56
- props.onUpdateBinding(index, {
61
+ onUpdateBinding(index, {
57
62
  ...binding,
58
63
  match: {
59
64
  ...binding.match,
@@ -65,7 +70,7 @@ export function RuntimeBindingListCard(props: {
65
70
  />
66
71
  </div>
67
72
  <div className="flex justify-end">
68
- <Button type="button" variant="outline" size="sm" onClick={() => props.onRemoveBinding(index)}>
73
+ <Button type="button" variant="outline" size="sm" onClick={() => onRemoveBinding(index)}>
69
74
  <Trash2 className="h-4 w-4 mr-1" />
70
75
  {t('remove')}
71
76
  </Button>
@@ -73,7 +78,7 @@ export function RuntimeBindingListCard(props: {
73
78
  </div>
74
79
  );
75
80
  })}
76
- <Button type="button" variant="outline" onClick={props.onAddBinding}>
81
+ <Button type="button" variant="outline" onClick={onAddBinding}>
77
82
  <Plus className="h-4 w-4 mr-2" />
78
83
  {t('addBinding')}
79
84
  </Button>
@@ -24,11 +24,14 @@ type UpdateRuntimeMutation = {
24
24
  isPending: boolean;
25
25
  };
26
26
 
27
- export function RuntimeConfigEditor(props: {
27
+ export function RuntimeConfigEditor({
28
+ config,
29
+ updateRuntime
30
+ }: {
28
31
  config: ConfigView;
29
32
  updateRuntime: UpdateRuntimeMutation;
30
33
  }) {
31
- const initialState = useMemo(() => createRuntimeConfigEditorState(props.config), [props.config]);
34
+ const initialState = useMemo(() => createRuntimeConfigEditorState(config), [config]);
32
35
  const [agents, setAgents] = useState(initialState.agents);
33
36
  const [bindings, setBindings] = useState(initialState.bindings);
34
37
  const [runtimeEntries, setRuntimeEntries] = useState(initialState.runtimeEntries);
@@ -68,7 +71,7 @@ export function RuntimeConfigEditor(props: {
68
71
  defaultEngine,
69
72
  knownAgentIds
70
73
  });
71
- props.updateRuntime.mutate({ data });
74
+ updateRuntime.mutate({ data });
72
75
  } catch (error) {
73
76
  toast.error(error instanceof Error ? error.message : String(error));
74
77
  }
@@ -109,9 +112,9 @@ export function RuntimeConfigEditor(props: {
109
112
  onAddBinding={() => setBindings((previous) => [...previous, createEmptyRuntimeBinding()])}
110
113
  />
111
114
  <div className="flex justify-end">
112
- <Button type="button" onClick={handleSave} disabled={props.updateRuntime.isPending}>
115
+ <Button type="button" onClick={handleSave} disabled={updateRuntime.isPending}>
113
116
  <Save className="h-4 w-4 mr-2" />
114
- {props.updateRuntime.isPending ? t('saving') : t('saveRuntimeSettings')}
117
+ {updateRuntime.isPending ? t('saving') : t('saveRuntimeSettings')}
115
118
  </Button>
116
119
  </div>
117
120
  </PageLayout>
@@ -6,7 +6,12 @@ import { Switch } from '@/shared/components/ui/switch';
6
6
  import { t } from '@/shared/lib/i18n';
7
7
  import type { RuntimeEntryDraft } from '@/features/system-status/utils/runtime-config-agent.utils';
8
8
 
9
- export function RuntimeEntryListCard(props: {
9
+ export function RuntimeEntryListCard({
10
+ entries,
11
+ onAddEntry,
12
+ onRemoveEntry,
13
+ onUpdateEntry
14
+ }: {
10
15
  entries: RuntimeEntryDraft[];
11
16
  onUpdateEntry: (index: number, patch: Partial<RuntimeEntryDraft>) => void;
12
17
  onRemoveEntry: (index: number) => void;
@@ -19,15 +24,15 @@ export function RuntimeEntryListCard(props: {
19
24
  <CardDescription>{t('runtimeEntriesHelp')}</CardDescription>
20
25
  </CardHeader>
21
26
  <CardContent className="space-y-3">
22
- {props.entries.map((entry, index) => (
27
+ {entries.map((entry, index) => (
23
28
  <div key={`${index}-${entry.id || 'runtime-entry'}`} className="rounded-xl border border-gray-200 p-3 space-y-3">
24
29
  <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
25
- <Input value={entry.id} onChange={(event) => props.onUpdateEntry(index, { id: event.target.value })} placeholder={t('runtimeEntryIdPlaceholder')} />
26
- <Input value={entry.label ?? ''} onChange={(event) => props.onUpdateEntry(index, { label: event.target.value })} placeholder={t('runtimeEntryLabelPlaceholder')} />
27
- <Input value={entry.type} onChange={(event) => props.onUpdateEntry(index, { type: event.target.value })} placeholder={t('runtimeEntryTypePlaceholder')} />
30
+ <Input value={entry.id} onChange={(event) => onUpdateEntry(index, { id: event.target.value })} placeholder={t('runtimeEntryIdPlaceholder')} />
31
+ <Input value={entry.label ?? ''} onChange={(event) => onUpdateEntry(index, { label: event.target.value })} placeholder={t('runtimeEntryLabelPlaceholder')} />
32
+ <Input value={entry.type} onChange={(event) => onUpdateEntry(index, { type: event.target.value })} placeholder={t('runtimeEntryTypePlaceholder')} />
28
33
  <div className="flex items-center justify-between rounded-lg border border-gray-200 px-3 py-2">
29
34
  <span className="text-sm text-gray-700">{t('enabled')}</span>
30
- <Switch checked={entry.enabled !== false} onCheckedChange={(checked) => props.onUpdateEntry(index, { enabled: checked })} />
35
+ <Switch checked={entry.enabled !== false} onCheckedChange={(checked) => onUpdateEntry(index, { enabled: checked })} />
31
36
  </div>
32
37
  </div>
33
38
  <div className="space-y-2">
@@ -35,19 +40,19 @@ export function RuntimeEntryListCard(props: {
35
40
  <textarea
36
41
  className="min-h-32 w-full rounded-md border border-gray-200 px-3 py-2 text-sm font-mono"
37
42
  value={entry.configText}
38
- onChange={(event) => props.onUpdateEntry(index, { configText: event.target.value })}
43
+ onChange={(event) => onUpdateEntry(index, { configText: event.target.value })}
39
44
  spellCheck={false}
40
45
  />
41
46
  </div>
42
47
  <div className="flex justify-end">
43
- <Button type="button" variant="outline" onClick={() => props.onRemoveEntry(index)}>
48
+ <Button type="button" variant="outline" onClick={() => onRemoveEntry(index)}>
44
49
  <Trash2 className="mr-2 h-4 w-4" />
45
50
  {t('delete')}
46
51
  </Button>
47
52
  </div>
48
53
  </div>
49
54
  ))}
50
- <Button type="button" variant="outline" onClick={props.onAddEntry}>
55
+ <Button type="button" variant="outline" onClick={onAddEntry}>
51
56
  <Plus className="mr-2 h-4 w-4" />
52
57
  {t('addRuntimeEntry')}
53
58
  </Button>
@@ -12,7 +12,16 @@ const DM_SCOPE_OPTIONS: Array<{ value: DmScope; labelKey: string }> = [
12
12
  { value: 'per-account-channel-peer', labelKey: 'dmScopePerAccountChannelPeer' }
13
13
  ];
14
14
 
15
- export function RuntimeSettingsCard(props: {
15
+ export function RuntimeSettingsCard({
16
+ companionEnabled,
17
+ defaultContextTokens,
18
+ defaultEngine,
19
+ dmScope,
20
+ onCompanionEnabledChange,
21
+ onDefaultContextTokensChange,
22
+ onDefaultEngineChange,
23
+ onDmScopeChange
24
+ }: {
16
25
  dmScope: DmScope;
17
26
  companionEnabled: boolean;
18
27
  defaultContextTokens: number;
@@ -34,7 +43,7 @@ export function RuntimeSettingsCard(props: {
34
43
  <div className="text-sm font-medium text-gray-800">{t('runtimeCompanionEnabled')}</div>
35
44
  <p className="text-xs text-gray-500">{t('runtimeCompanionEnabledHelp')}</p>
36
45
  </div>
37
- <Switch checked={props.companionEnabled} onCheckedChange={props.onCompanionEnabledChange} />
46
+ <Switch checked={companionEnabled} onCheckedChange={onCompanionEnabledChange} />
38
47
  </div>
39
48
  <div className="space-y-2">
40
49
  <label className="text-sm font-medium text-gray-800">{t('defaultContextTokens')}</label>
@@ -42,23 +51,23 @@ export function RuntimeSettingsCard(props: {
42
51
  type="number"
43
52
  min={1000}
44
53
  step={1000}
45
- value={props.defaultContextTokens}
46
- onChange={(event) => props.onDefaultContextTokensChange(Math.max(1000, Number.parseInt(event.target.value, 10) || 1000))}
54
+ value={defaultContextTokens}
55
+ onChange={(event) => onDefaultContextTokensChange(Math.max(1000, Number.parseInt(event.target.value, 10) || 1000))}
47
56
  />
48
57
  <p className="text-xs text-gray-500">{t('defaultContextTokensHelp')}</p>
49
58
  </div>
50
59
  <div className="space-y-2">
51
60
  <label className="text-sm font-medium text-gray-800">{t('defaultEngine')}</label>
52
61
  <Input
53
- value={props.defaultEngine}
54
- onChange={(event) => props.onDefaultEngineChange(event.target.value)}
62
+ value={defaultEngine}
63
+ onChange={(event) => onDefaultEngineChange(event.target.value)}
55
64
  placeholder={t('defaultEnginePlaceholder')}
56
65
  />
57
66
  <p className="text-xs text-gray-500">{t('defaultEngineHelp')}</p>
58
67
  </div>
59
68
  <div className="space-y-2">
60
69
  <label className="text-sm font-medium text-gray-800">{t('dmScope')}</label>
61
- <Select value={props.dmScope} onValueChange={(value) => props.onDmScopeChange(value as DmScope)}>
70
+ <Select value={dmScope} onValueChange={(value) => onDmScopeChange(value as DmScope)}>
62
71
  <SelectTrigger>
63
72
  <SelectValue />
64
73
  </SelectTrigger>