@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
@@ -0,0 +1,248 @@
1
+ import { PageHeader, PageLayout } from "@/components/layout/page-layout";
2
+ import { Button } from "@/components/ui/button";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "@/components/ui/card";
10
+ import { NoticeCard } from "@/components/ui/notice-card";
11
+ import { StatusDot } from "@/components/ui/status-dot";
12
+ import { useRemoteStatus } from "@/hooks/useRemoteAccess";
13
+ import { formatDateTime, t } from "@/lib/i18n";
14
+ import { useAppPresenter } from "@/presenter/app-presenter-context";
15
+ import { resolveRemoteWebBase } from "@/remote/remote-access.query";
16
+ import { buildRemoteAccessFeedbackView } from "@/remote/remote-access-feedback.service";
17
+ import { useRemoteAccessStore } from "@/remote/stores/remote-access.store";
18
+ import { Laptop, RefreshCcw, SquareArrowOutUpRight } from "lucide-react";
19
+ import { useEffect, useMemo } from "react";
20
+
21
+ function KeyValueRow(props: {
22
+ label: string;
23
+ value?: string | number | null;
24
+ muted?: boolean;
25
+ }) {
26
+ const { label, muted, value: rawValue } = props;
27
+ const value =
28
+ rawValue === undefined || rawValue === null || rawValue === ""
29
+ ? "-"
30
+ : String(rawValue);
31
+ return (
32
+ <div className="flex items-start justify-between gap-4 py-2 text-sm">
33
+ <span className="text-gray-500">{label}</span>
34
+ <span
35
+ className={
36
+ muted ? "text-right text-gray-500" : "text-right text-gray-900"
37
+ }
38
+ >
39
+ {value}
40
+ </span>
41
+ </div>
42
+ );
43
+ }
44
+
45
+ export function RemoteAccessPage() {
46
+ const presenter = useAppPresenter();
47
+ const remoteStatus = useRemoteStatus();
48
+ const status = remoteStatus.data;
49
+ const actionLabel = useRemoteAccessStore((state) => state.actionLabel);
50
+ const feedbackView = useMemo(
51
+ () => buildRemoteAccessFeedbackView(status),
52
+ [status],
53
+ );
54
+ const busy = Boolean(actionLabel);
55
+ const deviceName =
56
+ status?.runtime?.deviceName?.trim() ||
57
+ status?.settings.deviceName?.trim() ||
58
+ t("remoteDeviceNameAuto");
59
+ const canOpenDeviceList = Boolean(
60
+ status?.account.loggedIn && resolveRemoteWebBase(status),
61
+ );
62
+ const { hero: heroView, issueHint } = feedbackView;
63
+
64
+ useEffect(() => {
65
+ presenter.remoteAccessManager.syncStatus(status);
66
+ }, [presenter, status]);
67
+
68
+ if (remoteStatus.isLoading && !status) {
69
+ return <div className="p-8 text-gray-400">{t("remoteLoading")}</div>;
70
+ }
71
+
72
+ return (
73
+ <PageLayout className="space-y-6">
74
+ <PageHeader
75
+ title={t("remotePageTitle")}
76
+ description={t("remotePageDescription")}
77
+ />
78
+
79
+ <div className="grid gap-6 xl:grid-cols-[1.2fr_0.8fr]">
80
+ <Card>
81
+ <CardHeader className="space-y-4">
82
+ <div className="flex flex-wrap items-center gap-3">
83
+ <CardTitle>{heroView.title}</CardTitle>
84
+ <StatusDot
85
+ status={heroView.badgeStatus}
86
+ label={heroView.badgeLabel}
87
+ />
88
+ </div>
89
+ <CardDescription>{heroView.description}</CardDescription>
90
+ </CardHeader>
91
+ <CardContent className="space-y-5">
92
+ <NoticeCard tone="neutral">
93
+ <KeyValueRow
94
+ label={t("remoteSignedInAccount")}
95
+ value={status?.account.email}
96
+ />
97
+ <KeyValueRow label={t("remoteDeviceName")} value={deviceName} />
98
+ <KeyValueRow
99
+ label={t("remoteConnectionStatus")}
100
+ value={heroView.badgeLabel}
101
+ />
102
+ <KeyValueRow
103
+ label={t("remoteLastConnectedAt")}
104
+ value={
105
+ status?.runtime?.lastConnectedAt
106
+ ? formatDateTime(status.runtime.lastConnectedAt)
107
+ : "-"
108
+ }
109
+ muted
110
+ />
111
+ </NoticeCard>
112
+
113
+ <div className="flex flex-wrap gap-3">
114
+ {feedbackView.primaryAction ? (
115
+ <Button
116
+ onClick={() => {
117
+ if (feedbackView.primaryAction?.kind === "reauthorize") {
118
+ void presenter.remoteAccessManager.reauthorizeRemoteAccess(
119
+ status,
120
+ );
121
+ return;
122
+ }
123
+ if (feedbackView.primaryAction?.kind === "repair") {
124
+ void presenter.remoteAccessManager.repairRemoteAccess(
125
+ status,
126
+ );
127
+ return;
128
+ }
129
+ void presenter.remoteAccessManager.enableRemoteAccess(
130
+ status,
131
+ );
132
+ }}
133
+ disabled={busy}
134
+ >
135
+ {feedbackView.primaryAction.showRefreshIcon ? (
136
+ <RefreshCcw className="mr-2 h-4 w-4" />
137
+ ) : null}
138
+ {actionLabel || feedbackView.primaryAction.label}
139
+ </Button>
140
+ ) : null}
141
+
142
+ <Button
143
+ variant="outline"
144
+ onClick={() => void presenter.accountManager.openNextClawWeb()}
145
+ disabled={busy || !canOpenDeviceList}
146
+ >
147
+ <SquareArrowOutUpRight className="mr-2 h-4 w-4" />
148
+ {t("remoteOpenDeviceList")}
149
+ </Button>
150
+
151
+ {status?.settings.enabled ? (
152
+ <Button
153
+ variant="outline"
154
+ onClick={() =>
155
+ void presenter.remoteAccessManager.disableRemoteAccess(
156
+ status,
157
+ )
158
+ }
159
+ disabled={busy}
160
+ >
161
+ {t("remoteDisable")}
162
+ </Button>
163
+ ) : null}
164
+ </div>
165
+
166
+ {feedbackView.shouldShowIssueHint && issueHint ? (
167
+ <NoticeCard
168
+ tone="warning"
169
+ title={issueHint.title}
170
+ description={issueHint.body}
171
+ />
172
+ ) : null}
173
+
174
+ <p className="text-xs text-gray-500">{t("remoteOpenWebHint")}</p>
175
+ </CardContent>
176
+ </Card>
177
+
178
+ <Card>
179
+ <CardHeader>
180
+ <CardTitle className="flex items-center gap-2">
181
+ <Laptop className="h-4 w-4 text-primary" />
182
+ {t("remoteDeviceSectionTitle")}
183
+ </CardTitle>
184
+ <CardDescription>
185
+ {t("remoteDeviceSectionDescription")}
186
+ </CardDescription>
187
+ </CardHeader>
188
+ <CardContent className="space-y-5">
189
+ <div className="flex flex-wrap gap-2">
190
+ <StatusDot
191
+ status={status?.account.loggedIn ? "ready" : "inactive"}
192
+ label={
193
+ status?.account.loggedIn
194
+ ? t("remoteAccountConnected")
195
+ : t("remoteAccountNotConnected")
196
+ }
197
+ />
198
+ <StatusDot
199
+ status={status?.settings.enabled ? "active" : "inactive"}
200
+ label={
201
+ status?.settings.enabled
202
+ ? t("remoteEnabled")
203
+ : t("remoteStateDisabled")
204
+ }
205
+ />
206
+ <StatusDot
207
+ status={status?.service.running ? "active" : "inactive"}
208
+ label={
209
+ status?.service.running
210
+ ? t("remoteServiceRunning")
211
+ : t("remoteServiceStopped")
212
+ }
213
+ />
214
+ </div>
215
+
216
+ <NoticeCard tone="neutral">
217
+ <KeyValueRow label={t("remoteDeviceName")} value={deviceName} />
218
+ <KeyValueRow
219
+ label={t("remoteConnectionStatus")}
220
+ value={heroView.badgeLabel}
221
+ />
222
+ <KeyValueRow
223
+ label={t("remoteLastConnectedAt")}
224
+ value={
225
+ status?.runtime?.lastConnectedAt
226
+ ? formatDateTime(status.runtime.lastConnectedAt)
227
+ : "-"
228
+ }
229
+ muted
230
+ />
231
+ </NoticeCard>
232
+
233
+ <NoticeCard
234
+ tone="neutral"
235
+ borderStyle="dashed"
236
+ description={
237
+ status?.account.loggedIn
238
+ ? t("remoteOpenWebHint")
239
+ : t("remoteStatusNeedsSignInDescription")
240
+ }
241
+ className="text-sm"
242
+ />
243
+ </CardContent>
244
+ </Card>
245
+ </div>
246
+ </PageLayout>
247
+ );
248
+ }
@@ -0,0 +1,129 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const noticeCardVariants = cva("rounded-2xl border px-4 py-3", {
6
+ variants: {
7
+ tone: {
8
+ neutral: "border-gray-200 bg-gray-50 text-gray-900",
9
+ success: "border-emerald-200 bg-emerald-50 text-emerald-900",
10
+ warning: "border-amber-200 bg-amber-50 text-amber-900",
11
+ danger: "border-rose-200 bg-rose-50 text-rose-700",
12
+ info: "border-primary/20 bg-primary/10 text-primary",
13
+ },
14
+ borderStyle: {
15
+ solid: "",
16
+ dashed: "border-dashed",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ tone: "neutral",
21
+ borderStyle: "solid",
22
+ },
23
+ });
24
+
25
+ const titleClassMap: Record<
26
+ NonNullable<VariantProps<typeof noticeCardVariants>["tone"]>,
27
+ string
28
+ > = {
29
+ neutral: "text-gray-900",
30
+ success: "text-emerald-800",
31
+ warning: "text-amber-900",
32
+ danger: "text-rose-700",
33
+ info: "text-primary",
34
+ };
35
+
36
+ const descriptionClassMap: Record<
37
+ NonNullable<VariantProps<typeof noticeCardVariants>["tone"]>,
38
+ string
39
+ > = {
40
+ neutral: "text-gray-600",
41
+ success: "text-emerald-700",
42
+ warning: "text-amber-800",
43
+ danger: "text-rose-700",
44
+ info: "text-primary/90",
45
+ };
46
+
47
+ export interface NoticeCardProps
48
+ extends
49
+ Omit<React.HTMLAttributes<HTMLDivElement>, "title">,
50
+ VariantProps<typeof noticeCardVariants> {
51
+ title?: React.ReactNode;
52
+ description?: React.ReactNode;
53
+ icon?: React.ReactNode;
54
+ actions?: React.ReactNode;
55
+ }
56
+
57
+ export const NoticeCard = React.forwardRef<HTMLDivElement, NoticeCardProps>(
58
+ (
59
+ {
60
+ className,
61
+ tone = "neutral",
62
+ borderStyle = "solid",
63
+ title,
64
+ description,
65
+ icon,
66
+ actions,
67
+ children,
68
+ ...props
69
+ },
70
+ ref,
71
+ ) => {
72
+ const resolvedTone = tone ?? "neutral";
73
+ const hasHeader =
74
+ Boolean(title) ||
75
+ Boolean(description) ||
76
+ Boolean(icon) ||
77
+ Boolean(actions);
78
+
79
+ return (
80
+ <div
81
+ ref={ref}
82
+ className={cn(
83
+ noticeCardVariants({ tone: resolvedTone, borderStyle }),
84
+ className,
85
+ )}
86
+ {...props}
87
+ >
88
+ {hasHeader ? (
89
+ <div className="flex items-start justify-between gap-3">
90
+ <div className="min-w-0 flex-1">
91
+ <div className="flex items-start gap-2">
92
+ {icon ? <div className="mt-0.5 shrink-0">{icon}</div> : null}
93
+ <div className="min-w-0 flex-1">
94
+ {title ? (
95
+ <p
96
+ className={cn(
97
+ "text-sm font-medium",
98
+ titleClassMap[resolvedTone],
99
+ )}
100
+ >
101
+ {title}
102
+ </p>
103
+ ) : null}
104
+ {description ? (
105
+ <p
106
+ className={cn(
107
+ title ? "mt-1" : "",
108
+ "text-sm leading-6",
109
+ descriptionClassMap[resolvedTone],
110
+ )}
111
+ >
112
+ {description}
113
+ </p>
114
+ ) : null}
115
+ </div>
116
+ </div>
117
+ </div>
118
+ {actions ? <div className="shrink-0">{actions}</div> : null}
119
+ </div>
120
+ ) : null}
121
+ {children ? (
122
+ <div className={cn(hasHeader ? "mt-3" : "")}>{children}</div>
123
+ ) : null}
124
+ </div>
125
+ );
126
+ },
127
+ );
128
+
129
+ NoticeCard.displayName = "NoticeCard";
@@ -0,0 +1,51 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const settingRowVariants = cva(
6
+ "flex items-start justify-between gap-4 rounded-xl border p-4",
7
+ {
8
+ variants: {
9
+ tone: {
10
+ default: "border-gray-200 bg-white",
11
+ muted: "border-gray-200 bg-gray-50",
12
+ },
13
+ },
14
+ defaultVariants: {
15
+ tone: "default",
16
+ },
17
+ },
18
+ );
19
+
20
+ export interface SettingRowProps
21
+ extends
22
+ Omit<React.HTMLAttributes<HTMLDivElement>, "title">,
23
+ VariantProps<typeof settingRowVariants> {
24
+ title: React.ReactNode;
25
+ description?: React.ReactNode;
26
+ control?: React.ReactNode;
27
+ }
28
+
29
+ export const SettingRow = React.forwardRef<HTMLDivElement, SettingRowProps>(
30
+ (
31
+ { className, tone, title, description, control, children, ...props },
32
+ ref,
33
+ ) => (
34
+ <div
35
+ ref={ref}
36
+ className={cn(settingRowVariants({ tone }), className)}
37
+ {...props}
38
+ >
39
+ <div className="min-w-0 flex-1 space-y-2">
40
+ <div className="text-sm font-medium text-gray-900">{title}</div>
41
+ {description ? (
42
+ <p className="text-sm leading-6 text-gray-500">{description}</p>
43
+ ) : null}
44
+ {children}
45
+ </div>
46
+ {control ? <div className="shrink-0">{control}</div> : null}
47
+ </div>
48
+ ),
49
+ );
50
+
51
+ SettingRow.displayName = "SettingRow";
@@ -0,0 +1,39 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const tagChipVariants = cva(
6
+ "inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-medium",
7
+ {
8
+ variants: {
9
+ tone: {
10
+ subtle: "border-gray-200 bg-gray-50 text-gray-600",
11
+ neutral: "border-gray-200 bg-white text-gray-600",
12
+ success: "border-emerald-200 bg-emerald-50 text-emerald-700",
13
+ warning: "border-amber-200 bg-amber-50 text-amber-800",
14
+ danger: "border-rose-200 bg-rose-50 text-rose-600",
15
+ info: "border-primary/20 bg-primary/10 text-primary",
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ tone: "subtle",
20
+ },
21
+ },
22
+ );
23
+
24
+ export interface TagChipProps
25
+ extends
26
+ React.HTMLAttributes<HTMLSpanElement>,
27
+ VariantProps<typeof tagChipVariants> {}
28
+
29
+ export const TagChip = React.forwardRef<HTMLSpanElement, TagChipProps>(
30
+ ({ className, tone, ...props }, ref) => (
31
+ <span
32
+ ref={ref}
33
+ className={cn(tagChipVariants({ tone }), className)}
34
+ {...props}
35
+ />
36
+ ),
37
+ );
38
+
39
+ TagChip.displayName = "TagChip";
@@ -0,0 +1,19 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
5
+
6
+ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
7
+ ({ className, ...props }, ref) => (
8
+ <textarea
9
+ className={cn(
10
+ "flex min-h-28 w-full rounded-xl border border-gray-200/80 bg-white px-3.5 py-2.5 text-sm text-gray-900 placeholder:text-gray-300 placeholder:font-normal focus:outline-none focus:ring-1 focus:ring-primary/40 focus:border-primary/40 transition-colors disabled:cursor-not-allowed disabled:opacity-50",
11
+ className,
12
+ )}
13
+ ref={ref}
14
+ {...props}
15
+ />
16
+ ),
17
+ );
18
+
19
+ Textarea.displayName = "Textarea";
@@ -185,7 +185,8 @@ export function useUpdateChannel() {
185
185
  updateChannel(channel, data as Parameters<typeof updateChannel>[1]),
186
186
  onSuccess: () => {
187
187
  queryClient.invalidateQueries({ queryKey: ['config'] });
188
- toast.success(t('configSavedApplied'));
188
+ queryClient.invalidateQueries({ queryKey: ['config-meta'] });
189
+ toast.success(t('configSavedApplying'));
189
190
  },
190
191
  onError: (error: Error) => {
191
192
  toast.error(t('configSaveFailed') + ': ' + error.message);
package/src/index.css CHANGED
@@ -59,6 +59,30 @@
59
59
  background: hsl(var(--gray-400));
60
60
  }
61
61
 
62
+ .workspace-horizontal-scrollbar {
63
+ scrollbar-width: thin;
64
+ scrollbar-color: hsl(var(--gray-300) / 0.38) transparent;
65
+ scrollbar-gutter: stable;
66
+ }
67
+
68
+ .workspace-horizontal-scrollbar::-webkit-scrollbar {
69
+ width: 3px;
70
+ height: 2px;
71
+ }
72
+
73
+ .workspace-horizontal-scrollbar::-webkit-scrollbar-track {
74
+ background: transparent;
75
+ }
76
+
77
+ .workspace-horizontal-scrollbar::-webkit-scrollbar-thumb {
78
+ background: hsl(var(--gray-300) / 0.38);
79
+ border-radius: 999px;
80
+ }
81
+
82
+ .workspace-horizontal-scrollbar::-webkit-scrollbar-thumb:hover {
83
+ background: hsl(var(--gray-400) / 0.48);
84
+ }
85
+
62
86
  /* ========================================
63
87
  GLASSMORPHISM
64
88
  ======================================== */
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolveAppResourceUri } from "@/lib/app-resource-uri";
3
+
4
+ describe("resolveAppResourceUri", () => {
5
+ it("maps app resource uris to public app paths", () => {
6
+ expect(resolveAppResourceUri("app://runtime-icons/codex-openai.svg")).toBe(
7
+ "/runtime-icons/codex-openai.svg",
8
+ );
9
+ });
10
+
11
+ it("passes through ordinary image src values for compatibility", () => {
12
+ expect(resolveAppResourceUri("https://example.com/icon.png")).toBe(
13
+ "https://example.com/icon.png",
14
+ );
15
+ });
16
+
17
+ it("rejects app resource uris that escape the app resource directory", () => {
18
+ expect(resolveAppResourceUri("app://../icon.png")).toBeNull();
19
+ });
20
+ });
@@ -0,0 +1,29 @@
1
+ const APP_RESOURCE_URI_PREFIX = "app://";
2
+
3
+ function normalizeAppResourcePath(value: string): string | null {
4
+ const normalized = value.trim().replace(/^\/+/, "");
5
+ if (!normalized) {
6
+ return null;
7
+ }
8
+ const segments = normalized.split("/");
9
+ if (
10
+ segments.some((segment) => segment.trim().length === 0 || segment === "." || segment === "..")
11
+ ) {
12
+ return null;
13
+ }
14
+ return segments.join("/");
15
+ }
16
+
17
+ export function resolveAppResourceUri(uri: string): string | null {
18
+ const normalized = uri.trim();
19
+ if (!normalized) {
20
+ return null;
21
+ }
22
+ if (!normalized.startsWith(APP_RESOURCE_URI_PREFIX)) {
23
+ return normalized;
24
+ }
25
+ const appResourcePath = normalizeAppResourcePath(
26
+ normalized.slice(APP_RESOURCE_URI_PREFIX.length),
27
+ );
28
+ return appResourcePath ? `/${appResourcePath}` : null;
29
+ }
@@ -5,7 +5,7 @@ export const REMOTE_LABELS: Record<string, { zh: string; en: string }> = {
5
5
  en: 'Make this device appear in your NextClaw Platform device list and open it from the web.'
6
6
  },
7
7
  remoteOpenWeb: { zh: '前往 NextClaw Web', en: 'Open NextClaw Web' },
8
- remoteOpenDeviceList: { zh: '查看我的设备', en: 'View My Devices' },
8
+ remoteOpenDeviceList: { zh: '前往 NextClaw Web', en: 'Open NextClaw Web' },
9
9
  remoteOpenWebHint: {
10
10
  zh: '开启后,这台设备会出现在 NextClaw Web 中,你可以在那里点击打开并继续使用。',
11
11
  en: 'Once enabled, this device appears in NextClaw Web, where you can open it and keep working.'
@@ -75,6 +75,37 @@ export const RUNTIME_CONTROL_LABELS: Record<string, { zh: string; en: string }>
75
75
  runtimeControlRestartService: { zh: '重启服务', en: 'Restart Service' },
76
76
  runtimeControlStopService: { zh: '停止服务', en: 'Stop Service' },
77
77
  runtimeControlRestartApp: { zh: '重启应用', en: 'Restart App' },
78
+ runtimeControlPendingRestartTitle: { zh: '待重启', en: 'Pending Restart' },
79
+ runtimeControlPendingRestartDescription: {
80
+ zh: '这次改动已经保存,但系统不会自动重启。请在你方便的时候手动重启,重启完成后该提示会自动清空。',
81
+ en: 'These changes are saved, but the system will not restart automatically. Restart manually when you are ready, and this notice clears after the restart finishes.'
82
+ },
83
+ runtimeControlPendingRestartPaths: { zh: '待生效项', en: 'Changes Waiting For Restart' },
84
+ runtimeStatusLoadingTitle: { zh: '读取状态中', en: 'Loading status' },
85
+ runtimeStatusLoadingDescription: {
86
+ zh: '正在读取当前系统状态。',
87
+ en: 'Loading the current system status.'
88
+ },
89
+ runtimeStatusHealthyTitle: { zh: '系统正常', en: 'System healthy' },
90
+ runtimeStatusHealthyDescription: {
91
+ zh: '当前没有需要你立即处理的系统动作。',
92
+ en: 'There is no system action that needs your attention right now.'
93
+ },
94
+ runtimeStatusPendingRestartTitle: { zh: '待重启', en: 'Restart required' },
95
+ runtimeStatusPendingRestartDescription: {
96
+ zh: '这些改动已经保存,但不会自动重启。你可以在这里查看原因,并在方便的时候手动重启。',
97
+ en: 'These changes are saved, but the system will not restart automatically. Review the reason here and restart when you are ready.'
98
+ },
99
+ runtimeStatusPendingRestartReasonItem: {
100
+ zh: '{path} 改动将在重启后生效。',
101
+ en: 'Changes in {path} will apply after restart.'
102
+ },
103
+ runtimeStatusActionHint: {
104
+ zh: '准备好时再执行',
105
+ en: 'Run when you are ready'
106
+ },
107
+ runtimeStatusRestartAction: { zh: '立即重启', en: 'Restart now' },
108
+ runtimeStatusRestartingAction: { zh: '重启中...', en: 'Restarting...' },
78
109
  runtimeControlStartingServiceHelp: {
79
110
  zh: '正在启动 NextClaw 服务,页面可能会在服务恢复后重新连接。',
80
111
  en: 'Starting the NextClaw service. The page may reconnect after the service becomes available.'
package/src/lib/i18n.ts CHANGED
@@ -497,22 +497,19 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
497
497
 
498
498
  // Remote & Status
499
499
  ...REMOTE_LABELS,
500
-
501
- // Action labels
502
500
  actionConfigure: { zh: '配置', en: 'Configure' },
503
501
  actionAddProvider: { zh: '添加提供商', en: 'Add Provider' },
504
502
  actionEnable: { zh: '启用', en: 'Enable' },
505
-
506
- // Messages
507
503
  configSaved: { zh: '配置已保存', en: 'Configuration saved' },
504
+ configSavedApplying: { zh: '配置已保存,正在应用', en: 'Configuration saved, applying changes' },
508
505
  configSavedApplied: { zh: '配置已保存并已应用', en: 'Configuration saved and applied' },
509
506
  configSaveFailed: { zh: '保存配置失败', en: 'Failed to save configuration' },
510
507
  configReloaded: { zh: '配置已重载', en: 'Configuration reloaded' },
511
508
  configReloadFailed: { zh: '重载配置失败', en: 'Failed to reload configuration' },
512
- feishuVerifySuccess: {
513
- zh: '验证成功,请到飞书开放平台完成事件订阅与发布后再开始使用。',
514
- en: 'Verified. Please finish Feishu event subscription and app publishing before using.'
515
- },
509
+ channelConfigApplying: { zh: '渠道配置正在应用', en: 'Channel configuration is applying' },
510
+ channelConfigApplied: { zh: '渠道配置已应用', en: 'Channel configuration applied' },
511
+ channelConfigApplyFailed: { zh: '渠道配置应用失败', en: 'Failed to apply channel configuration' },
512
+ feishuVerifySuccess: { zh: '验证成功,请到飞书开放平台完成事件订阅与发布后再开始使用。', en: 'Verified. Please finish Feishu event subscription and app publishing before using.' },
516
513
  feishuVerifyFailed: { zh: '验证失败', en: 'Verification failed' },
517
514
  enterTag: { zh: '输入后按回车...', en: 'Type and press Enter...' },
518
515
  headerName: { zh: 'Header 名称', en: 'Header Name' },