@nextclaw/ui 0.15.21 → 0.15.22

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 (161) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/assets/{api-CHo2gekJ.js → api-B1yHQYs-.js} +3 -3
  3. package/dist/assets/app-presenter-provider-uP9waOzv.js +108 -0
  4. package/dist/assets/appearance-settings-page-C9Zq3hP9.js +1 -0
  5. package/dist/assets/{book-open-BrZL4o9L.js → book-open-oSmyVXea.js} +1 -1
  6. package/dist/assets/{card-B5rRLu_6.js → card-CGvJZTZp.js} +1 -1
  7. package/dist/assets/{channels-list-page-C1dnm68I.js → channels-list-page-5BTxpQHw.js} +2 -2
  8. package/dist/assets/chat-page-jTvli8Jp.js +1 -0
  9. package/dist/assets/config-split-page-BgBsUmp1.js +1 -0
  10. package/dist/assets/confirm-dialog-h849QRWO.js +5 -0
  11. package/dist/assets/desktop-update-config-BGGHxClR.js +1 -0
  12. package/dist/assets/{dist-C9EX46wM.js → dist-CxUhurq9.js} +1 -1
  13. package/dist/assets/doc-browser-0eL8AX42.js +1 -0
  14. package/dist/assets/{doc-browser-DH-3hDnI.js → doc-browser-DEqE9lsh.js} +1 -1
  15. package/dist/assets/{doc-browser-context-TU2heon0.js → doc-browser-context-DkzFIUoK.js} +1 -1
  16. package/dist/assets/{eye-B-3dyZNA.js → eye-gOWQGt8N.js} +1 -1
  17. package/dist/assets/{form-actions-DvvxCpL4.js → form-actions-CgjYZUzM.js} +1 -1
  18. package/dist/assets/{index-k_eWScvN.js → index-DSAUWCfl.js} +2 -2
  19. package/dist/assets/index-G7cBryJ4.css +1 -0
  20. package/dist/assets/loader-circle-BAuycGf6.js +1 -0
  21. package/dist/assets/{logo-badge-DB51c0cS.js → logo-badge-BNcsDsUM.js} +1 -1
  22. package/dist/assets/{mcp-marketplace-page-DWtsIqml.js → mcp-marketplace-page-BX39MtPa.js} +3 -3
  23. package/dist/assets/mcp-marketplace-page-CkDtOEA4.js +1 -0
  24. package/dist/assets/{model-config-page-D6cGNnMM.js → model-config-page-7MIXg2fQ.js} +1 -1
  25. package/dist/assets/{navigation-link-BSJEUiwD.js → navigation-link-B3iFOdxx.js} +1 -1
  26. package/dist/assets/plus-yvhFgfal.js +1 -0
  27. package/dist/assets/{popover-Bp9IpE_N.js → popover-Cpep_rJe.js} +1 -1
  28. package/dist/assets/{provider-models-CXUgDxGR.js → provider-models-CUrptZ-o.js} +1 -1
  29. package/dist/assets/provider-scoped-model-input-CHmOzz7x.js +1 -0
  30. package/dist/assets/providers-config-page-A6J7Y0II.js +1 -0
  31. package/dist/assets/react-BCJGCjrP.js +8 -0
  32. package/dist/assets/{refresh-cw-C2e94pmp.js → refresh-cw-CkKwPztT.js} +1 -1
  33. package/dist/assets/remote-FD-ZfSIn.js +1 -0
  34. package/dist/assets/rotate-cw-DrJMfHhp.js +1 -0
  35. package/dist/assets/{runtime-config-page-C57OLdPm.js → runtime-config-page-DwjO-Vs6.js} +1 -1
  36. package/dist/assets/{save-CkwDOgvt.js → save-BINMWate.js} +1 -1
  37. package/dist/assets/{search-CLXM2reO.js → search-7M38azRp.js} +1 -1
  38. package/dist/assets/{search-config-page-tnJXaaSr.js → search-config-page-T6tMhXbP.js} +1 -1
  39. package/dist/assets/{secrets-config-page-DQ_Curaj.js → secrets-config-page-2mVejXf2.js} +2 -2
  40. package/dist/assets/security-config-DD5RrjDe.js +1 -0
  41. package/dist/assets/{select-CNFBf00l.js → select-BvidP5Oa.js} +1 -1
  42. package/dist/assets/{settings-2-B9IT2HGd.js → settings-2-DqZstREV.js} +1 -1
  43. package/dist/assets/skeleton-BEQW2Dva.js +1 -0
  44. package/dist/assets/{tabs-BuNr8LSO.js → tabs-BeLxVmkt.js} +1 -1
  45. package/dist/assets/{tag-chip-CDkB5smU.js → tag-chip-BoDaLnWt.js} +1 -1
  46. package/dist/assets/{trash-2-GchaJuEM.js → trash-2-CYUskMMN.js} +1 -1
  47. package/dist/assets/{use-config-C69rR15i.js → use-config-CxKRXXCl.js} +1 -1
  48. package/dist/assets/use-confirm-dialog-HASTvOP2.js +1 -0
  49. package/dist/assets/{use-viewport-layout-BACNZyMY.js → use-viewport-layout-CJxkjoRz.js} +1 -1
  50. package/dist/assets/x-DdUwjre5.js +1 -0
  51. package/dist/index.html +29 -29
  52. package/package.json +7 -7
  53. package/src/app/components/layout/sidebar-rail.styles.ts +0 -2
  54. package/src/app/components/layout/sidebar.tsx +2 -2
  55. package/src/app/styles/design-system.css +12 -0
  56. package/src/features/chat/components/chat-sidebar-list-mode-switch.tsx +2 -2
  57. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +19 -1
  58. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +1 -1
  59. package/src/features/chat/components/conversation/chat-conversation-content.tsx +5 -6
  60. package/src/features/chat/components/conversation/chat-conversation-header.tsx +1 -1
  61. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +1 -1
  62. package/src/features/chat/components/layout/__tests__/chat-sidebar-session-area.test.tsx +52 -0
  63. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +11 -17
  64. package/src/features/chat/components/layout/chat-page-shell.tsx +2 -11
  65. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +32 -42
  66. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +13 -64
  67. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +1 -4
  68. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -1
  69. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +15 -4
  70. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +7 -7
  71. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +4 -2
  72. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +1 -0
  73. package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +1 -0
  74. package/src/features/chat/features/conversation/utils/session-queued-input.utils.ts +1 -0
  75. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +3 -0
  76. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +0 -1
  77. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +3 -0
  78. package/src/features/chat/features/message/utils/__tests__/chat-message-timeline.utils.test.ts +29 -0
  79. package/src/features/chat/features/message/utils/chat-message-timeline.utils.ts +4 -1
  80. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +10 -10
  81. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +2 -2
  82. package/src/features/chat/features/session/components/session-header/chat-session-header-menu-item.tsx +1 -1
  83. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +1 -1
  84. package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +1 -1
  85. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +1 -1
  86. package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +26 -0
  87. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +4 -2
  88. package/src/features/chat/features/session/utils/chat-session-display.utils.ts +48 -12
  89. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +13 -4
  90. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +6 -0
  91. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +10 -7
  92. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +1 -1
  93. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +3 -14
  94. package/src/features/chat/features/welcome/components/chat-welcome-prompt-suggestions.tsx +37 -0
  95. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +1 -1
  96. package/src/features/chat/features/welcome/components/chat-welcome.tsx +33 -31
  97. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -1
  98. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +1 -1
  99. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +15 -3
  100. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +3 -10
  101. package/src/features/chat/managers/chat-session-list.manager.ts +3 -23
  102. package/src/features/chat/managers/chat-thread.manager.ts +2 -5
  103. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +3 -9
  104. package/src/features/chat/pages/ncp-chat-page.tsx +0 -1
  105. package/src/features/chat/types/chat-input-bar.types.ts +1 -0
  106. package/src/features/cron/components/cron-task-discovery.tsx +1 -1
  107. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  108. package/src/features/notifications/components/__tests__/app-notification-toast.test.tsx +3 -0
  109. package/src/features/notifications/components/app-notification-toast.tsx +1 -1
  110. package/src/features/panel-apps/components/panel-app-list-item.tsx +1 -1
  111. package/src/features/panel-apps/components/panel-app-toolbar.tsx +2 -2
  112. package/src/features/panel-apps/components/panel-apps-list.tsx +4 -4
  113. package/src/features/service-apps/components/service-apps-panel.tsx +1 -1
  114. package/src/features/settings/components/config/provider-auth-section.tsx +90 -59
  115. package/src/features/settings/components/config/provider-form.tsx +10 -17
  116. package/src/features/settings/components/config/provider-status-badge.tsx +4 -2
  117. package/src/features/settings/pages/__tests__/providers-config-page.test.tsx +12 -0
  118. package/src/features/settings/pages/providers-config-page.tsx +2 -1
  119. package/src/platforms/mobile/components/__tests__/mobile-app-shell.test.tsx +13 -7
  120. package/src/platforms/mobile/components/__tests__/mobile-bottom-nav.test.tsx +18 -20
  121. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +1 -1
  122. package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +1 -1
  123. package/src/shared/components/path-picker/server-path-picker-locations.tsx +1 -1
  124. package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +3 -3
  125. package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +1 -1
  126. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
  127. package/src/shared/components/ui/button.tsx +4 -4
  128. package/src/shared/components/ui/dialog.tsx +1 -1
  129. package/src/shared/components/ui/input.tsx +1 -1
  130. package/src/shared/components/ui/scroll-area.tsx +2 -0
  131. package/src/shared/components/ui/select.tsx +1 -1
  132. package/src/shared/components/ui/switch.tsx +1 -1
  133. package/src/shared/components/ui/tabs.tsx +1 -1
  134. package/src/shared/components/ui/textarea.tsx +1 -1
  135. package/src/shared/lib/api/ncp-session.types.ts +1 -0
  136. package/src/shared/lib/api/types.ts +2 -0
  137. package/src/shared/lib/i18n/locales/en-US/chat.json +13 -14
  138. package/src/shared/lib/i18n/locales/zh-CN/chat.json +13 -14
  139. package/src/shared/lib/provider-models/__tests__/anonymous-provider-configuration.test.ts +40 -0
  140. package/src/shared/lib/provider-models/index.ts +4 -3
  141. package/dist/assets/app-presenter-provider-2_DmWYT9.js +0 -108
  142. package/dist/assets/appearance-settings-page-BEy2UE7R.js +0 -1
  143. package/dist/assets/chat-page-Dz6tvZ91.js +0 -1
  144. package/dist/assets/config-split-page-6y6W6dz4.js +0 -1
  145. package/dist/assets/confirm-dialog-q1CpEmR8.js +0 -5
  146. package/dist/assets/desktop-update-config-DrdlwP4j.js +0 -1
  147. package/dist/assets/doc-browser-BfttDSti.js +0 -1
  148. package/dist/assets/index-CXDPLDFD.css +0 -1
  149. package/dist/assets/loader-circle-B35NlHTI.js +0 -1
  150. package/dist/assets/mcp-marketplace-page-CkPtwPBk.js +0 -1
  151. package/dist/assets/plus-BRVe5jSJ.js +0 -1
  152. package/dist/assets/provider-scoped-model-input-dk7fz9NL.js +0 -1
  153. package/dist/assets/providers-config-page-DTP2YfxA.js +0 -1
  154. package/dist/assets/react-D6ShNq95.js +0 -8
  155. package/dist/assets/remote-FllhXW3o.js +0 -1
  156. package/dist/assets/rotate-cw-CTC-Qszi.js +0 -1
  157. package/dist/assets/security-config-S00DjFfO.js +0 -1
  158. package/dist/assets/skeleton-Dc06gatK.js +0 -1
  159. package/dist/assets/use-confirm-dialog-DF_EgPYx.js +0 -1
  160. package/dist/assets/x-BXFquY9e.js +0 -1
  161. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +0 -59
@@ -1,4 +1,5 @@
1
1
  import { Button } from '@/shared/components/ui/button';
2
+ import { MaskedInput } from '@/shared/components/common/masked-input';
2
3
  import { Label } from '@/shared/components/ui/label';
3
4
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
4
5
  import { t } from '@/shared/lib/i18n';
@@ -6,6 +7,10 @@ import type { ProviderTemplateView } from '@/shared/lib/api';
6
7
  import { ProviderPillSelector } from './provider-pill-selector';
7
8
 
8
9
  type ProviderAuthSectionProps = {
10
+ apiKey: string;
11
+ apiKeyRequired: boolean;
12
+ apiKeySet: boolean;
13
+ apiKeyPlaceholder: string;
9
14
  providerAuth: ProviderTemplateView['auth'];
10
15
  providerAuthNote: string;
11
16
  providerAuthMethodOptions: Array<{ value: string; label: string }>;
@@ -20,10 +25,15 @@ type ProviderAuthSectionProps = {
20
25
  importPending: boolean;
21
26
  authSessionId: string | null;
22
27
  authStatusMessage: string;
28
+ onApiKeyChange: (value: string) => void;
23
29
  };
24
30
 
25
31
  export function ProviderAuthSection(props: ProviderAuthSectionProps) {
26
32
  const {
33
+ apiKey,
34
+ apiKeyRequired,
35
+ apiKeySet,
36
+ apiKeyPlaceholder,
27
37
  providerAuth,
28
38
  providerAuthNote,
29
39
  providerAuthMethodOptions,
@@ -37,75 +47,96 @@ export function ProviderAuthSection(props: ProviderAuthSectionProps) {
37
47
  startPending,
38
48
  importPending,
39
49
  authSessionId,
40
- authStatusMessage
50
+ authStatusMessage,
51
+ onApiKeyChange
41
52
  } = props;
42
53
 
43
- if (providerAuth?.kind !== 'device_code') return null;
54
+ const apiKeyField = apiKeyRequired ? (
55
+ <div className='space-y-2'>
56
+ <Label htmlFor='apiKey' className='text-sm font-medium text-foreground'>
57
+ {t('apiKey')}
58
+ </Label>
59
+ <MaskedInput
60
+ id='apiKey'
61
+ value={apiKey}
62
+ isSet={apiKeySet}
63
+ onChange={(event) => onApiKeyChange(event.target.value)}
64
+ placeholder={apiKeyPlaceholder}
65
+ className='rounded-xl'
66
+ />
67
+ <p className='text-xs text-muted-foreground'>{t('leaveBlankToKeepUnchanged')}</p>
68
+ </div>
69
+ ) : null;
70
+
71
+ if (providerAuth?.kind !== 'device_code') return apiKeyField;
44
72
 
45
73
  return (
46
- <div className='space-y-2 rounded-xl bg-muted/45 p-3'>
47
- <Label className='text-sm font-medium text-foreground'>
48
- {providerAuth.displayName || t('providerAuthSectionTitle')}
49
- </Label>
50
- {providerAuthNote ? <p className='text-xs text-muted-foreground'>{providerAuthNote}</p> : null}
51
- {providerAuthMethodsCount > 1 ? (
52
- <div className='space-y-2'>
53
- <Label className='text-xs font-medium text-foreground'>{t('providerAuthMethodLabel')}</Label>
54
- {shouldUseAuthMethodPills ? (
55
- <ProviderPillSelector
56
- value={resolvedAuthMethodId}
57
- onChange={onAuthMethodChange}
58
- options={providerAuthMethodOptions}
59
- />
60
- ) : (
61
- <Select value={resolvedAuthMethodId} onValueChange={onAuthMethodChange}>
62
- <SelectTrigger className='h-8 rounded-lg'>
63
- <SelectValue placeholder={t('providerAuthMethodPlaceholder')} />
64
- </SelectTrigger>
65
- <SelectContent>
66
- {providerAuthMethodOptions.map((method) => (
67
- <SelectItem key={method.value} value={method.value}>
68
- {method.label}
69
- </SelectItem>
70
- ))}
71
- </SelectContent>
72
- </Select>
73
- )}
74
- {selectedAuthMethodHint ? <p className='text-xs text-muted-foreground'>{selectedAuthMethodHint}</p> : null}
75
- </div>
76
- ) : null}
77
- <div className='flex flex-wrap items-center gap-2'>
78
- <Button
79
- type='button'
80
- variant='outline'
81
- size='sm'
82
- onClick={onStartProviderAuth}
83
- disabled={startPending || Boolean(authSessionId)}
84
- >
85
- {startPending
86
- ? t('providerAuthStarting')
87
- : authSessionId
88
- ? t('providerAuthAuthorizing')
89
- : t('providerAuthAuthorizeInBrowser')}
90
- </Button>
91
- {providerAuth.supportsCliImport ? (
74
+ <>
75
+ {apiKeyField}
76
+ <div className='space-y-2 rounded-xl bg-muted/45 p-3'>
77
+ <Label className='text-sm font-medium text-foreground'>
78
+ {providerAuth.displayName || t('providerAuthSectionTitle')}
79
+ </Label>
80
+ {providerAuthNote ? <p className='text-xs text-muted-foreground'>{providerAuthNote}</p> : null}
81
+ {providerAuthMethodsCount > 1 ? (
82
+ <div className='space-y-2'>
83
+ <Label className='text-xs font-medium text-foreground'>{t('providerAuthMethodLabel')}</Label>
84
+ {shouldUseAuthMethodPills ? (
85
+ <ProviderPillSelector
86
+ value={resolvedAuthMethodId}
87
+ onChange={onAuthMethodChange}
88
+ options={providerAuthMethodOptions}
89
+ />
90
+ ) : (
91
+ <Select value={resolvedAuthMethodId} onValueChange={onAuthMethodChange}>
92
+ <SelectTrigger className='h-8 rounded-lg'>
93
+ <SelectValue placeholder={t('providerAuthMethodPlaceholder')} />
94
+ </SelectTrigger>
95
+ <SelectContent>
96
+ {providerAuthMethodOptions.map((method) => (
97
+ <SelectItem key={method.value} value={method.value}>
98
+ {method.label}
99
+ </SelectItem>
100
+ ))}
101
+ </SelectContent>
102
+ </Select>
103
+ )}
104
+ {selectedAuthMethodHint ? <p className='text-xs text-muted-foreground'>{selectedAuthMethodHint}</p> : null}
105
+ </div>
106
+ ) : null}
107
+ <div className='flex flex-wrap items-center gap-2'>
92
108
  <Button
93
109
  type='button'
94
110
  variant='outline'
95
111
  size='sm'
96
- onClick={onImportProviderAuthFromCli}
97
- disabled={importPending}
112
+ onClick={onStartProviderAuth}
113
+ disabled={startPending || Boolean(authSessionId)}
98
114
  >
99
- {importPending ? t('providerAuthImporting') : t('providerAuthImportFromCli')}
115
+ {startPending
116
+ ? t('providerAuthStarting')
117
+ : authSessionId
118
+ ? t('providerAuthAuthorizing')
119
+ : t('providerAuthAuthorizeInBrowser')}
100
120
  </Button>
101
- ) : null}
102
- {authSessionId ? (
103
- <span className='text-xs text-muted-foreground'>
104
- {t('providerAuthSessionLabel')}: {authSessionId.slice(0, 8)}…
105
- </span>
106
- ) : null}
121
+ {providerAuth.supportsCliImport ? (
122
+ <Button
123
+ type='button'
124
+ variant='outline'
125
+ size='sm'
126
+ onClick={onImportProviderAuthFromCli}
127
+ disabled={importPending}
128
+ >
129
+ {importPending ? t('providerAuthImporting') : t('providerAuthImportFromCli')}
130
+ </Button>
131
+ ) : null}
132
+ {authSessionId ? (
133
+ <span className='text-xs text-muted-foreground'>
134
+ {t('providerAuthSessionLabel')}: {authSessionId.slice(0, 8)}…
135
+ </span>
136
+ ) : null}
137
+ </div>
138
+ {authStatusMessage ? <p className='text-xs text-muted-foreground'>{authStatusMessage}</p> : null}
107
139
  </div>
108
- {authStatusMessage ? <p className='text-xs text-muted-foreground'>{authStatusMessage}</p> : null}
109
- </div>
140
+ </>
110
141
  );
111
142
  }
@@ -9,7 +9,6 @@ import {
9
9
  useTestProviderConnection,
10
10
  useUpdateProvider
11
11
  } from '@/shared/hooks/use-config';
12
- import { MaskedInput } from '@/shared/components/common/masked-input';
13
12
  import { Button } from '@/shared/components/ui/button';
14
13
  import { Input } from '@/shared/components/ui/input';
15
14
  import { Label } from '@/shared/components/ui/label';
@@ -445,7 +444,11 @@ function ProviderFormDetailPane(props: ProviderFormDetailPaneProps) {
445
444
  >
446
445
  <Trash2 className='h-4 w-4' />
447
446
  </button>
448
- <ProviderStatusBadge enabled={context.currentEnabled} apiKeySet={context.resolvedProviderConfig.apiKeySet} />
447
+ <ProviderStatusBadge
448
+ enabled={context.currentEnabled}
449
+ apiKeyRequired={context.resolvedProviderConfig.apiKeyRequired}
450
+ apiKeySet={context.resolvedProviderConfig.apiKeySet}
451
+ />
449
452
  </div>
450
453
  </div>
451
454
  </ConfigSplitPaneHeader>
@@ -467,22 +470,11 @@ function ProviderFormDetailPane(props: ProviderFormDetailPaneProps) {
467
470
  <p className='text-xs text-muted-foreground'>{t('providerDisplayNameHelpShort')}</p>
468
471
  </div>
469
472
 
470
- <div className='space-y-2'>
471
- <Label htmlFor='apiKey' className='text-sm font-medium text-foreground'>
472
- {t('apiKey')}
473
- </Label>
474
- <MaskedInput
475
- id='apiKey'
476
- value={apiKey}
477
- isSet={context.resolvedProviderConfig.apiKeySet}
478
- onChange={(e) => onApiKeyChange(e.target.value)}
479
- placeholder={context.apiKeyHint?.placeholder ?? t('enterApiKey')}
480
- className='rounded-xl'
481
- />
482
- <p className='text-xs text-muted-foreground'>{t('leaveBlankToKeepUnchanged')}</p>
483
- </div>
484
-
485
473
  <ProviderAuthSection
474
+ apiKey={apiKey}
475
+ apiKeyRequired={context.resolvedProviderConfig.apiKeyRequired !== false}
476
+ apiKeySet={context.resolvedProviderConfig.apiKeySet}
477
+ apiKeyPlaceholder={context.apiKeyHint?.placeholder ?? t('enterApiKey')}
486
478
  providerAuth={context.providerAuth}
487
479
  providerAuthNote={context.providerAuth?.note?.[language] || context.providerAuth?.note?.en || context.providerAuth?.displayName || ''}
488
480
  providerAuthMethodOptions={context.providerAuthMethodOptions}
@@ -497,6 +489,7 @@ function ProviderFormDetailPane(props: ProviderFormDetailPaneProps) {
497
489
  importPending={importPending}
498
490
  authSessionId={authSessionId}
499
491
  authStatusMessage={authStatusMessage}
492
+ onApiKeyChange={onApiKeyChange}
500
493
  />
501
494
 
502
495
  <div className='space-y-2'>
@@ -3,13 +3,15 @@ import { t } from '@/shared/lib/i18n';
3
3
 
4
4
  type ProviderStatusBadgeProps = {
5
5
  enabled: boolean;
6
+ apiKeyRequired?: boolean;
6
7
  apiKeySet: boolean;
7
8
  className?: string;
8
9
  };
9
10
 
10
- export function ProviderStatusBadge({ apiKeySet, className, enabled }: ProviderStatusBadgeProps) {
11
+ export function ProviderStatusBadge({ apiKeyRequired = true, apiKeySet, className, enabled }: ProviderStatusBadgeProps) {
11
12
  if (!enabled) {
12
13
  return <StatusDot status="inactive" label={t('disabled')} className={className} />;
13
14
  }
14
- return <StatusDot status={apiKeySet ? 'ready' : 'setup'} label={apiKeySet ? t('statusReady') : t('statusSetup')} className={className} />;
15
+ const isReady = !apiKeyRequired || apiKeySet;
16
+ return <StatusDot status={isReady ? 'ready' : 'setup'} label={isReady ? t('statusReady') : t('statusSetup')} className={className} />;
15
17
  }
@@ -11,6 +11,7 @@ const mocks = vi.hoisted(() => ({
11
11
  providers: {
12
12
  openai: { enabled: true, apiKeySet: true },
13
13
  anthropic: { enabled: true, apiKeySet: true },
14
+ opencode: { enabled: true, apiKeyRequired: false, apiKeySet: false },
14
15
  nextclaw: { enabled: true, apiKeySet: true },
15
16
  },
16
17
  },
@@ -22,6 +23,7 @@ const mocks = vi.hoisted(() => ({
22
23
  { name: 'nextclaw', displayName: 'NextClaw Builtin', defaultApiBase: 'https://ai-gateway-api.nextclaw.io/v1' },
23
24
  { name: 'openai', displayName: 'OpenAI', defaultApiBase: 'https://api.openai.com/v1' },
24
25
  { name: 'anthropic', displayName: 'Anthropic', defaultApiBase: 'https://api.anthropic.com/v1' },
26
+ { name: 'opencode', displayName: 'OpenCode Zen Free Trial', defaultApiBase: 'https://opencode.ai/zen/v1' },
25
27
  ],
26
28
  },
27
29
  },
@@ -46,6 +48,7 @@ vi.mock('@/shared/hooks/use-config', () => ({
46
48
  isBuiltInType: providerId === 'nextclaw',
47
49
  isCustom: false,
48
50
  enabled: provider.enabled,
51
+ apiKeyRequired: 'apiKeyRequired' in provider ? provider.apiKeyRequired : undefined,
49
52
  apiKeySet: provider.apiKeySet,
50
53
  },
51
54
  ]),
@@ -108,4 +111,13 @@ describe('ProvidersConfigPage', () => {
108
111
  expect.stringContaining('NextClaw Builtin'),
109
112
  ]);
110
113
  });
114
+
115
+ it('shows an enabled provider without a required API key as ready', () => {
116
+ const { container } = render(<ProvidersConfigPage />);
117
+ const opencodeCard = Array.from(container.querySelectorAll('[role="button"]'))
118
+ .find((element) => element.textContent?.includes('OpenCode Zen Free Trial'));
119
+
120
+ expect(opencodeCard?.textContent).toContain('Ready');
121
+ expect(opencodeCard?.textContent).not.toContain('Setup');
122
+ });
111
123
  });
@@ -17,6 +17,7 @@ import { ConfirmDialog } from '@/shared/components/ui/confirm-dialog';
17
17
  import { StatusDot } from '@/shared/components/status/status-dot';
18
18
  import { t } from '@/shared/lib/i18n';
19
19
  import { cn } from '@/shared/lib/utils';
20
+ import { isProviderConfigured } from '@/shared/lib/provider-models';
20
21
  import { ProviderForm } from '@/features/settings/components/config/provider-form';
21
22
  import { useViewportLayout } from '@/app/hooks/use-viewport-layout';
22
23
  import {
@@ -232,7 +233,7 @@ export function ProvidersConfigPage() {
232
233
  {filteredProviders.map((provider) => {
233
234
  const template = provider.providerType ? templateByType.get(provider.providerType) : null;
234
235
  const isEnabled = provider.enabled !== false;
235
- const isReady = Boolean(isEnabled && provider.apiKeySet);
236
+ const isReady = isProviderConfigured(provider);
236
237
  const providerLabel = provider.displayName?.trim() || template?.displayName || provider.providerId;
237
238
  const description =
238
239
  formatBasePreview(provider.apiBase || template?.defaultApiBase || '') ||
@@ -1,4 +1,5 @@
1
1
  import { render, screen } from "@testing-library/react";
2
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
3
  import { MemoryRouter } from "react-router-dom";
3
4
  import { describe, expect, it, vi } from "vitest";
4
5
  import { I18nProvider } from "@/app/components/i18n-provider";
@@ -9,14 +10,19 @@ vi.mock("@/shared/components/doc-browser", () => ({
9
10
  }));
10
11
 
11
12
  function renderShell(pathname: string) {
13
+ const queryClient = new QueryClient({
14
+ defaultOptions: { queries: { retry: false } },
15
+ });
12
16
  const view = render(
13
- <I18nProvider>
14
- <MemoryRouter initialEntries={[pathname]}>
15
- <MobileAppShell pathname={pathname} isDocBrowserOpen={false}>
16
- <div>Content</div>
17
- </MobileAppShell>
18
- </MemoryRouter>
19
- </I18nProvider>,
17
+ <QueryClientProvider client={queryClient}>
18
+ <I18nProvider>
19
+ <MemoryRouter initialEntries={[pathname]}>
20
+ <MobileAppShell pathname={pathname} isDocBrowserOpen={false}>
21
+ <div>Content</div>
22
+ </MobileAppShell>
23
+ </MemoryRouter>
24
+ </I18nProvider>
25
+ </QueryClientProvider>,
20
26
  );
21
27
  return {
22
28
  ...view,
@@ -1,4 +1,5 @@
1
1
  import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
3
  import { MemoryRouter } from "react-router-dom";
3
4
  import { describe, expect, it, vi } from "vitest";
4
5
  import { I18nProvider } from "@/app/components/i18n-provider";
@@ -11,15 +12,24 @@ vi.mock("@/shared/components/doc-browser", () => ({
11
12
  useDocBrowser: () => ({ open: vi.fn() }),
12
13
  }));
13
14
 
14
- describe("MobileBottomNav", () => {
15
- it("highlights the settings tab for nested settings routes", () => {
16
- render(
15
+ function renderBottomNav(pathname: string) {
16
+ const queryClient = new QueryClient({
17
+ defaultOptions: { queries: { retry: false } },
18
+ });
19
+ return render(
20
+ <QueryClientProvider client={queryClient}>
17
21
  <I18nProvider>
18
- <MemoryRouter initialEntries={["/providers"]}>
22
+ <MemoryRouter initialEntries={[pathname]}>
19
23
  <MobileBottomNav />
20
24
  </MemoryRouter>
21
- </I18nProvider>,
22
- );
25
+ </I18nProvider>
26
+ </QueryClientProvider>,
27
+ );
28
+ }
29
+
30
+ describe("MobileBottomNav", () => {
31
+ it("highlights the settings tab for nested settings routes", () => {
32
+ renderBottomNav("/providers");
23
33
 
24
34
  expect(
25
35
  screen.getByRole("link", { name: /settings/i }).getAttribute("aria-current"),
@@ -37,13 +47,7 @@ describe("MobileBottomNav", () => {
37
47
  });
38
48
 
39
49
  it("highlights the chat tab for chat routes", () => {
40
- render(
41
- <I18nProvider>
42
- <MemoryRouter initialEntries={["/chat/demo-session"]}>
43
- <MobileBottomNav />
44
- </MemoryRouter>
45
- </I18nProvider>,
46
- );
50
+ renderBottomNav("/chat/demo-session");
47
51
 
48
52
  expect(
49
53
  screen.getByRole("link", { name: /chat/i }).getAttribute("aria-current"),
@@ -51,13 +55,7 @@ describe("MobileBottomNav", () => {
51
55
  });
52
56
 
53
57
  it("opens the apps panel from the mobile navigation", () => {
54
- render(
55
- <I18nProvider>
56
- <MemoryRouter initialEntries={["/chat"]}>
57
- <MobileBottomNav />
58
- </MemoryRouter>
59
- </I18nProvider>,
60
- );
58
+ renderBottomNav("/chat");
61
59
 
62
60
  fireEvent.click(screen.getByRole("button", { name: "Apps" }));
63
61
 
@@ -55,7 +55,7 @@ export function DocBrowserAddressToolbar({
55
55
  onChange={(e) => onUrlInputChange(e.target.value)}
56
56
  aria-label={t('docBrowserAddressLabel')}
57
57
  placeholder={placeholder}
58
- className="h-8 w-full rounded-lg border border-border/75 bg-background pl-8 pr-3 text-xs text-foreground transition-colors placeholder:text-muted-foreground/55 focus:border-border focus:outline-none focus:ring-0"
58
+ className="h-8 w-full rounded-lg border border-border/75 bg-background pl-8 pr-3 text-xs text-foreground transition-colors placeholder:text-muted-foreground/55 focus:outline-none focus:ring-0"
59
59
  />
60
60
  </form>
61
61
  <IconActionButton
@@ -63,7 +63,7 @@ export function ServerPathPickerDirectoryList({
63
63
  'grid w-full grid-cols-[minmax(0,1fr)_8rem] items-center rounded-sm border border-transparent px-2 py-1.5 text-left text-sm text-foreground outline-none transition-colors',
64
64
  selected
65
65
  ? 'border-primary/25 bg-primary/15'
66
- : 'hover:bg-accent/70 focus-visible:border-primary/35 focus-visible:bg-accent/70',
66
+ : 'hover:bg-[var(--interaction-hover)] focus-visible:border-primary/35 focus-visible:bg-accent/70',
67
67
  )}
68
68
  onClick={() => onSelect(entry.path)}
69
69
  onDoubleClick={() => onOpen(entry.path)}
@@ -112,7 +112,7 @@ export function ServerPathPickerLocations({
112
112
  type="button"
113
113
  className={cn(
114
114
  'flex w-full items-center gap-2 rounded-md px-2 py-2 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border',
115
- active ? 'bg-primary/12 text-primary' : 'text-foreground hover:bg-accent',
115
+ active ? 'bg-primary/12 text-primary' : 'text-foreground hover:bg-[var(--interaction-hover)]',
116
116
  )}
117
117
  aria-current={active ? 'location' : undefined}
118
118
  onClick={() => onNavigate(item.path)}
@@ -99,7 +99,7 @@ export function ServerPathPickerNavigationBar({
99
99
  disabled={!canGoUp || disabled}
100
100
  />
101
101
 
102
- <div className="ml-1 flex h-9 min-w-0 flex-1 items-center overflow-hidden rounded-md border border-border bg-background shadow-sm focus-within:ring-1 focus-within:ring-primary/50">
102
+ <div className="ml-1 flex h-9 min-w-0 flex-1 items-center overflow-hidden rounded-md border border-border bg-background shadow-sm">
103
103
  {editingAddress ? (
104
104
  <Input
105
105
  value={addressDraft}
@@ -136,7 +136,7 @@ export function ServerPathPickerNavigationBar({
136
136
  <>
137
137
  <button
138
138
  type="button"
139
- className="rounded px-1.5 py-1 text-sm text-muted-foreground hover:bg-accent"
139
+ className="rounded px-1.5 py-1 text-sm text-muted-foreground hover:bg-[var(--interaction-hover)]"
140
140
  aria-label={t('pathPickerShowFullAddress')}
141
141
  onClick={beginAddressEdit}
142
142
  >
@@ -149,7 +149,7 @@ export function ServerPathPickerNavigationBar({
149
149
  ) : null}
150
150
  <button
151
151
  type="button"
152
- className="max-w-40 truncate rounded px-1.5 py-1 text-sm text-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
152
+ className="max-w-40 truncate rounded px-1.5 py-1 text-sm text-foreground hover:bg-[var(--interaction-hover)] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
153
153
  onClick={() => onNavigate(breadcrumb.path)}
154
154
  disabled={disabled}
155
155
  >
@@ -31,7 +31,7 @@ describe('IconActionButton', () => {
31
31
  const button = screen.getByRole('button', { name: 'More' });
32
32
  expect(button.className).toContain('h-7');
33
33
  expect(button.className).toContain('w-7');
34
- expect(button.className).toContain('hover:bg-accent');
34
+ expect(button.className).toContain('hover:bg-[var(--interaction-hover)]');
35
35
  });
36
36
 
37
37
  it('ignores restored pointer focus while preserving keyboard tooltip focus', () => {
@@ -31,7 +31,7 @@ const SIZE_CLASS: Record<IconActionButtonSize, string> = {
31
31
 
32
32
  const TONE_CLASS: Record<IconActionButtonTone, string> = {
33
33
  default:
34
- 'text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:text-muted-foreground/45 disabled:hover:bg-transparent disabled:hover:text-muted-foreground/45',
34
+ 'text-muted-foreground hover:bg-[var(--interaction-hover)] hover:text-accent-foreground disabled:text-muted-foreground/45 disabled:hover:bg-transparent disabled:hover:text-muted-foreground/45',
35
35
  strong:
36
36
  'text-muted-foreground hover:bg-black/10 hover:text-foreground disabled:text-muted-foreground/45 disabled:hover:bg-transparent disabled:hover:text-muted-foreground/45',
37
37
  };
@@ -9,12 +9,12 @@ const buttonVariants = cva(
9
9
  variant: {
10
10
  default: 'bg-primary text-primary-foreground hover:bg-primary-600 active:bg-primary-700 shadow-sm',
11
11
  destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
12
- outline: 'border border-border bg-card text-muted-foreground hover:bg-accent hover:text-accent-foreground',
13
- secondary: 'bg-muted text-foreground hover:bg-accent hover:text-accent-foreground',
14
- ghost: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
12
+ outline: 'border border-border bg-card text-muted-foreground hover:bg-[var(--interaction-hover)] hover:text-accent-foreground',
13
+ secondary: 'bg-muted text-foreground hover:bg-[var(--interaction-hover)] hover:text-accent-foreground',
14
+ ghost: 'text-muted-foreground hover:bg-[var(--interaction-hover)] hover:text-accent-foreground',
15
15
  link: 'text-primary underline-offset-4 hover:underline',
16
16
  primary: 'bg-primary text-primary-foreground hover:bg-primary-600 active:bg-primary-700 shadow-sm',
17
- subtle: 'bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground',
17
+ subtle: 'bg-muted text-muted-foreground hover:bg-[var(--interaction-hover)] hover:text-accent-foreground',
18
18
  'primary-outline': 'border border-primary/30 text-primary hover:bg-primary hover:text-primary-foreground'
19
19
  },
20
20
  size: {
@@ -42,7 +42,7 @@ const DialogContent = React.forwardRef<
42
42
  {...props}
43
43
  >
44
44
  {children}
45
- <DialogPrimitive.Close className="absolute right-4 top-4 rounded-lg p-1 opacity-70 transition-all duration-fast hover:bg-accent hover:text-accent-foreground hover:opacity-100 focus:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:pointer-events-none">
45
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-lg p-1 opacity-70 transition-all duration-fast hover:bg-[var(--interaction-hover)] hover:text-accent-foreground hover:opacity-100 focus:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:pointer-events-none">
46
46
  <X className="h-4 w-4 text-muted-foreground" />
47
47
  <span className="sr-only">Close</span>
48
48
  </DialogPrimitive.Close>
@@ -9,7 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
9
9
  <input
10
10
  type={type}
11
11
  className={cn(
12
- 'flex h-9 w-full rounded-xl border border-border/75 bg-card px-3.5 py-2 text-sm text-foreground file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground/55 placeholder:font-normal transition-colors focus:outline-none focus:ring-0 focus:border-border disabled:cursor-not-allowed disabled:opacity-50',
12
+ 'flex h-9 w-full rounded-xl border border-border/75 bg-card px-3.5 py-2 text-sm text-foreground file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground/55 placeholder:font-normal transition-colors focus:outline-none focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50',
13
13
  className
14
14
  )}
15
15
  ref={ref}
@@ -5,6 +5,8 @@ interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
5
5
  children: React.ReactNode;
6
6
  }
7
7
 
8
+ export const SCROLL_BOTTOM_EDGE_FADE_CLASS = '[mask-image:linear-gradient(to_bottom,black_calc(100%_-_28px),transparent)] [-webkit-mask-image:linear-gradient(to_bottom,black_calc(100%_-_28px),transparent)]';
9
+
8
10
  const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
9
11
  ({ className, children, ...props }, ref) => (
10
12
  <div
@@ -119,7 +119,7 @@ const SelectItem = React.forwardRef<
119
119
  <SelectPrimitive.Item
120
120
  ref={ref}
121
121
  className={cn(
122
- "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-accent",
122
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-[var(--interaction-hover)]",
123
123
  className
124
124
  )}
125
125
  {...props}
@@ -18,7 +18,7 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
18
18
  ref={ref}
19
19
  className={cn(
20
20
  'switch-track peer inline-flex h-[22px] w-10 shrink-0 cursor-pointer items-center rounded-full border-2 transition-colors duration-fast focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-50',
21
- checked ? 'border-primary/70 bg-primary' : 'border-border/60 bg-muted hover:bg-accent',
21
+ checked ? 'border-primary/70 bg-primary' : 'border-border/60 bg-muted hover:bg-[var(--interaction-hover)]',
22
22
  className
23
23
  )}
24
24
  onClick={() => onCheckedChange?.(!checked)}
@@ -67,7 +67,7 @@ export function TabsTrigger({ value, children, className }: TabsTriggerProps) {
67
67
  'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-[13px] font-medium transition-all duration-fast focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:pointer-events-none disabled:opacity-50',
68
68
  isActive
69
69
  ? 'bg-card text-foreground shadow-sm'
70
- : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
70
+ : 'text-muted-foreground hover:bg-[var(--interaction-hover)] hover:text-accent-foreground',
71
71
  className
72
72
  )}
73
73
  >
@@ -7,7 +7,7 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
7
7
  ({ className, ...props }, ref) => (
8
8
  <textarea
9
9
  className={cn(
10
- "flex min-h-28 w-full rounded-xl border border-border/75 bg-card px-3.5 py-2.5 text-sm text-foreground placeholder:text-muted-foreground/55 placeholder:font-normal transition-colors focus:outline-none focus:ring-0 focus:border-border disabled:cursor-not-allowed disabled:opacity-50",
10
+ "flex min-h-28 w-full rounded-xl border border-border/75 bg-card px-3.5 py-2.5 text-sm text-foreground placeholder:text-muted-foreground/55 placeholder:font-normal transition-colors focus:outline-none focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50",
11
11
  className,
12
12
  )}
13
13
  ref={ref}
@@ -39,6 +39,7 @@ export type SessionContextWindowView = {
39
39
  export type SessionActivityPreviewView = {
40
40
  state: "running" | "completed" | "failed" | "cancelled" | "idle";
41
41
  timestamp: string;
42
+ statusKind?: "thinking" | "tool-running" | "tool-completed" | "run-failed" | "run-interrupted";
42
43
  statusText?: string;
43
44
  replyText?: string;
44
45
  };
@@ -68,6 +68,7 @@ export type ProviderInstanceView = {
68
68
  isCustom: boolean;
69
69
  enabled: boolean;
70
70
  displayName?: string;
71
+ apiKeyRequired?: boolean;
71
72
  apiKeySet: boolean;
72
73
  apiKeyMasked?: string;
73
74
  apiBase?: string | null;
@@ -621,6 +622,7 @@ export type ProviderTemplateView = {
621
622
  envKey: string;
622
623
  isGateway?: boolean;
623
624
  isLocal?: boolean;
625
+ apiKeyRequired?: boolean;
624
626
  defaultApiBase?: string;
625
627
  logo?: string;
626
628
  apiBaseHelp?: {