@nextclaw/ui 0.15.26 → 0.15.27

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 (125) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/assets/{api-CK3Fro7z.js → api-a_GaOFc7.js} +4 -4
  3. package/dist/assets/{appearance-settings-page-zqwWO8Dy.js → appearance-settings-page-DlGXLgff.js} +1 -1
  4. package/dist/assets/{channels-list-page-CVCZBlCL.js → channels-list-page-BEEk4Uc9.js} +2 -2
  5. package/dist/assets/{chat-page-D4wehRTz.js → chat-page-DUQXXqQm.js} +1 -1
  6. package/dist/assets/{config-split-page-CceNFvlY.js → config-split-page-ps1Aod3g.js} +1 -1
  7. package/dist/assets/{confirm-dialog-CWeNYWZY.js → confirm-dialog-DJSUNG33.js} +2 -2
  8. package/dist/assets/desktop-update-config--zagjHic.js +1 -0
  9. package/dist/assets/{dist-D9xtGgdL.js → dist-BkTYNzm_.js} +1 -1
  10. package/dist/assets/{dist-AYxkFx2B.js → dist-DkR7_Dpl.js} +1 -1
  11. package/dist/assets/doc-browser-DahzJEWj.js +1 -0
  12. package/dist/assets/doc-browser-Q67TOGVS.js +1 -0
  13. package/dist/assets/{doc-browser-context-Cb7Z0j90.js → doc-browser-context-BQim8SqD.js} +1 -1
  14. package/dist/assets/form-actions-B2-bpFie.js +1 -0
  15. package/dist/assets/index-Ba_o5fu9.js +109 -0
  16. package/dist/assets/index-DSLK7zgR.css +1 -0
  17. package/dist/assets/loader-circle-DyXZU92l.js +1 -0
  18. package/dist/assets/mcp-marketplace-page-B0dBE5CV.js +1 -0
  19. package/dist/assets/mcp-marketplace-page-w8panjZG.js +9 -0
  20. package/dist/assets/{model-config-page-DfOLyQqR.js → model-config-page-rrhFaque.js} +1 -1
  21. package/dist/assets/{navigation-link-MiiyoIjH.js → navigation-link-mpHFXwRr.js} +2 -2
  22. package/dist/assets/plus-Dhb4KuMH.js +1 -0
  23. package/dist/assets/{provider-scoped-model-input-DXzjr5l0.js → provider-scoped-model-input-CFsfWEt9.js} +1 -1
  24. package/dist/assets/{providers-config-page-CaO8WfZH.js → providers-config-page-D-TYFZsF.js} +1 -1
  25. package/dist/assets/react-BlXwvOLo.js +8 -0
  26. package/dist/assets/{refresh-cw-a2VVeeAF.js → refresh-cw-JFuyDj_P.js} +1 -1
  27. package/dist/assets/remote-BwVv_Qq-.js +1 -0
  28. package/dist/assets/rotate-cw-DjbbiIW_.js +1 -0
  29. package/dist/assets/{runtime-config-page-BuA4A02z.js → runtime-config-page-Br9j9Vxe.js} +1 -1
  30. package/dist/assets/{save-CGm23Yww.js → save-BL8P8CLX.js} +1 -1
  31. package/dist/assets/{search-CH2t--sH.js → search-bZL75Pgc.js} +1 -1
  32. package/dist/assets/{search-config-page-BAmYqoAz.js → search-config-page-B3fvzMd1.js} +1 -1
  33. package/dist/assets/{secrets-config-page-BlPOuql_.js → secrets-config-page-ClwTxtXd.js} +2 -2
  34. package/dist/assets/security-config-DmJw9QZA.js +1 -0
  35. package/dist/assets/{select-DnxxDpcV.js → select-rUZ8nIZu.js} +2 -2
  36. package/dist/assets/{settings-2-agnMYc8a.js → settings-2-BI4SCkxQ.js} +1 -1
  37. package/dist/assets/skeleton-CVmEPG43.js +1 -0
  38. package/dist/assets/{tag-chip-CPOc8b2N.js → tag-chip-BdI_yIOA.js} +1 -1
  39. package/dist/assets/x-BcEayhV6.js +1 -0
  40. package/dist/index.html +19 -19
  41. package/package.json +7 -7
  42. package/src/app/components/layout/app-layout.tsx +38 -2
  43. package/src/app/hooks/__tests__/use-app-event-consumers.test.ts +20 -0
  44. package/src/app/hooks/use-app-event-consumers.ts +21 -0
  45. package/src/app/presenters/app.presenter.ts +6 -0
  46. package/src/features/apps/components/__tests__/app-packages-panel.test.tsx +92 -14
  47. package/src/features/apps/components/__tests__/apps-panel.test.tsx +58 -0
  48. package/src/features/apps/components/app-artwork.tsx +49 -0
  49. package/src/features/apps/components/app-marketplace-catalog.tsx +332 -0
  50. package/src/features/apps/components/app-marketplace-cover.tsx +64 -0
  51. package/src/features/apps/components/app-marketplace-detail.tsx +161 -0
  52. package/src/features/apps/components/app-marketplace-dialog.tsx +107 -217
  53. package/src/features/apps/components/app-marketplace-operation.tsx +84 -0
  54. package/src/features/apps/components/app-package-card.tsx +201 -52
  55. package/src/features/apps/components/app-packages-panel.tsx +98 -26
  56. package/src/features/apps/components/apps-panel.tsx +28 -27
  57. package/src/features/apps/hooks/use-app-marketplace.test.ts +144 -0
  58. package/src/features/apps/hooks/use-app-marketplace.ts +55 -85
  59. package/src/features/apps/hooks/use-app-packages.ts +59 -4
  60. package/src/features/apps/index.ts +1 -0
  61. package/src/features/apps/managers/__tests__/app-package-operation-settlement.manager.test.ts +81 -0
  62. package/src/features/apps/managers/app-package-operation-settlement.manager.ts +80 -0
  63. package/src/features/apps/services/app-marketplace.service.ts +223 -0
  64. package/src/features/apps/types/app-marketplace.types.ts +75 -0
  65. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +64 -3
  66. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +24 -14
  67. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +53 -0
  68. package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx +18 -0
  69. package/src/features/chat/features/conversation/hooks/use-chat-composer-reference-intent.ts +4 -1
  70. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +126 -32
  71. package/src/features/chat/features/conversation/hooks/use-session-run-queue.ts +12 -2
  72. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +31 -0
  73. package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +5 -0
  74. package/src/features/chat/features/input/utils/chat-inline-token-composer.utils.ts +14 -0
  75. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +25 -0
  76. package/src/features/chat/features/message/hooks/use-chat-inline-token-actions.ts +8 -1
  77. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +77 -20
  78. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +56 -50
  79. package/src/features/chat/features/workspace/components/project-files/workspace-directory-tree.tsx +33 -11
  80. package/src/features/chat/features/workspace/components/project-files/workspace-project-tree-view.tsx +93 -0
  81. package/src/features/chat/features/workspace/stores/__tests__/workspace-project-tree.store.test.ts +38 -0
  82. package/src/features/chat/features/workspace/stores/workspace-project-tree.store.ts +160 -0
  83. package/src/features/chat/index.ts +1 -0
  84. package/src/features/chat/managers/__tests__/chat-composer-intent.manager.test.ts +29 -0
  85. package/src/features/chat/managers/chat-composer-intent.manager.ts +29 -2
  86. package/src/features/chat/presenters/chat.presenter.ts +2 -2
  87. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +1 -1
  88. package/src/features/panel-apps/components/panel-apps-list.tsx +1 -6
  89. package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts +34 -0
  90. package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +29 -0
  91. package/src/features/service-apps/components/service-apps-panel.tsx +3 -17
  92. package/src/platforms/desktop/components/desktop-app-shell.tsx +5 -0
  93. package/src/platforms/mobile/components/mobile-app-shell.tsx +9 -1
  94. package/src/shared/components/actions/__tests__/navigation-link.test.tsx +12 -0
  95. package/src/shared/components/actions/navigation-link.tsx +6 -3
  96. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +34 -0
  97. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +5 -0
  98. package/src/shared/components/doc-browser/doc-browser.tsx +11 -1
  99. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +13 -0
  100. package/src/shared/components/ui/tabs.tsx +24 -1
  101. package/src/shared/hooks/use-server-path-watch.ts +42 -0
  102. package/src/shared/lib/api/server-path/server-path.types.ts +10 -0
  103. package/src/shared/lib/api/types.ts +3 -0
  104. package/src/shared/lib/api/utils/server-path.utils.ts +10 -0
  105. package/src/shared/lib/i18n/locales/en-US/chat.json +1 -0
  106. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +49 -7
  107. package/src/shared/lib/i18n/locales/zh-CN/chat.json +1 -0
  108. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +49 -7
  109. package/vite.config.ts +32 -1
  110. package/dist/assets/desktop-update-config-Dirc3bpt.js +0 -1
  111. package/dist/assets/doc-browser-4Sud6dWw.js +0 -1
  112. package/dist/assets/doc-browser-D4x_yH5F.js +0 -1
  113. package/dist/assets/form-actions-B2q4jgjE.js +0 -1
  114. package/dist/assets/index-CdyTsSBu.css +0 -1
  115. package/dist/assets/index-Das_bw2h.js +0 -107
  116. package/dist/assets/loader-circle-CpmBdaon.js +0 -1
  117. package/dist/assets/mcp-marketplace-page-CdOY3Sop.js +0 -1
  118. package/dist/assets/mcp-marketplace-page-DvjrfqEt.js +0 -9
  119. package/dist/assets/plus-C8FkjpSZ.js +0 -1
  120. package/dist/assets/react-C8XFFvuz.js +0 -8
  121. package/dist/assets/remote-BJHXa4mB.js +0 -1
  122. package/dist/assets/rotate-cw-DJ7UF3F2.js +0 -1
  123. package/dist/assets/security-config-CbDiY2xr.js +0 -1
  124. package/dist/assets/skeleton-XpqeWL62.js +0 -1
  125. package/dist/assets/x-odTHAtLZ.js +0 -1
@@ -1,83 +1,133 @@
1
- import { useMemo, useState } from 'react';
2
- import type { AppPackageView } from '@nextclaw/client-sdk';
3
- import { AppWindow, Check, ExternalLink, Search, ShieldCheck } from 'lucide-react';
4
- import { useAppMarketplace } from '@/features/apps/hooks/use-app-marketplace';
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import type { AppPackageOperationView, AppPackageView } from '@nextclaw/client-sdk';
3
+ import { ArrowLeft, ShieldCheck } from 'lucide-react';
5
4
  import {
6
- buildLocaleFallbacks,
7
- pickLocalizedText,
8
- } from '@/features/marketplace';
9
- import { Button } from '@/shared/components/ui/button';
5
+ AppMarketplaceCatalog,
6
+ type MarketplaceFilter,
7
+ } from '@/features/apps/components/app-marketplace-catalog';
8
+ import { MarketplaceDetail } from '@/features/apps/components/app-marketplace-detail';
9
+ import { findLatestAppPackageOperation } from '@/features/apps/components/app-marketplace-operation';
10
+ import {
11
+ useAppMarketplace,
12
+ useAppMarketplaceDetail,
13
+ } from '@/features/apps/hooks/use-app-marketplace';
14
+ import { buildLocaleFallbacks } from '@/features/marketplace';
10
15
  import {
11
16
  Dialog,
12
17
  DialogContent,
13
- DialogDescription,
14
- DialogHeader,
15
- DialogTitle,
16
18
  } from '@/shared/components/ui/dialog';
17
- import { Input } from '@/shared/components/ui/input';
18
- import { Skeleton } from '@/shared/components/ui/skeleton';
19
19
  import { getLanguage, t } from '@/shared/lib/i18n';
20
20
 
21
+ const HIDDEN_ENGINEERING_APPS = new Set([
22
+ 'nextclaw.starter-card',
23
+ 'nextclaw.validation-task-board',
24
+ ]);
25
+
21
26
  export function AppMarketplaceDialog({
22
27
  error,
23
28
  installedPackages,
24
- installingSource,
25
- isPending,
29
+ isStarting,
26
30
  onInstall,
27
31
  onOpenChange,
32
+ onUpdate,
28
33
  open,
34
+ operations,
35
+ startingSource,
29
36
  }: {
30
37
  error: Error | null;
31
38
  installedPackages: AppPackageView[];
32
- installingSource?: string;
33
- isPending: boolean;
39
+ isStarting: boolean;
34
40
  onInstall: (source: string, registryUrl: string) => void;
35
41
  onOpenChange: (open: boolean) => void;
42
+ onUpdate: (appId: string) => void;
36
43
  open: boolean;
44
+ operations: AppPackageOperationView[];
45
+ startingSource?: string;
37
46
  }) {
47
+ const [filter, setFilter] = useState<MarketplaceFilter>('all');
38
48
  const [search, setSearch] = useState('');
39
- const marketplace = useAppMarketplace(open);
40
- const installedAppIds = useMemo(
41
- () => new Set(installedPackages.map((entry) => entry.id)),
49
+ const [debouncedSearch, setDebouncedSearch] = useState('');
50
+ const [selectedSlug, setSelectedSlug] = useState<string | null>(null);
51
+ useEffect(() => {
52
+ const timer = window.setTimeout(() => setDebouncedSearch(search.trim()), 250);
53
+ return () => window.clearTimeout(timer);
54
+ }, [search]);
55
+ const marketplaceParams = useMemo(() => ({
56
+ q: debouncedSearch || undefined,
57
+ featured: filter === 'featured' ? true : undefined,
58
+ tags: filter === 'personal'
59
+ ? ['personal', 'productivity', 'notes', 'calendar']
60
+ : undefined,
61
+ tag: filter === 'local'
62
+ ? 'local'
63
+ : undefined,
64
+ sort: debouncedSearch ? 'relevance' as const : 'featured' as const,
65
+ }), [debouncedSearch, filter]);
66
+ const marketplace = useAppMarketplace(marketplaceParams, open);
67
+ const detail = useAppMarketplaceDetail(selectedSlug, open);
68
+ const installedById = useMemo(
69
+ () => new Map(installedPackages.map((entry) => [entry.id, entry])),
42
70
  [installedPackages],
43
71
  );
44
72
  const localeFallbacks = buildLocaleFallbacks(getLanguage() === 'zh' ? 'zh-CN' : 'en-US');
45
- const items = useMemo(() => {
46
- const query = search.trim().toLocaleLowerCase();
47
- const availableItems = marketplace.data?.items ?? [];
48
- if (!query) return availableItems;
49
- return availableItems.filter((item) => [
50
- item.name,
51
- item.appId,
52
- item.summary,
53
- pickLocalizedText(item.summaryI18n, item.summary, localeFallbacks),
54
- ...item.tags,
55
- ].some((value) => value.toLocaleLowerCase().includes(query)));
56
- }, [localeFallbacks, marketplace.data?.items, search]);
73
+ const items = useMemo(
74
+ () => (marketplace.data?.items ?? []).filter(
75
+ (item) => !HIDDEN_ENGINEERING_APPS.has(item.appId),
76
+ ),
77
+ [marketplace.data?.items],
78
+ );
79
+
80
+ const closeDialog = (nextOpen: boolean) => {
81
+ if (!nextOpen) setSelectedSlug(null);
82
+ onOpenChange(nextOpen);
83
+ };
57
84
 
58
85
  return (
59
- <Dialog open={open} onOpenChange={onOpenChange}>
60
- <DialogContent className="flex max-h-[calc(100vh-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-h-[720px] sm:max-w-2xl">
61
- <MarketplaceHeader search={search} onSearchChange={setSearch} />
62
- <div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto bg-muted/15 px-3 py-3 sm:px-4">
63
- {error ? (
64
- <div role="alert" className="mb-3 rounded-xl bg-destructive/8 px-3 py-2.5 text-xs leading-5 text-destructive ring-1 ring-destructive/15">
65
- {error.message || t('appPackagesActionFailed')}
86
+ <Dialog open={open} onOpenChange={closeDialog}>
87
+ <DialogContent className="flex h-dvh w-screen max-w-none flex-col gap-0 overflow-hidden rounded-none border-0 bg-background/95 p-0 shadow-[0_32px_100px_rgba(0,0,0,0.24)] backdrop-blur-xl [&>button.absolute]:top-[max(1rem,env(safe-area-inset-top))] sm:h-[min(680px,calc(100dvh-4rem))] sm:w-[min(920px,calc(100vw-3rem))] sm:max-w-none sm:rounded-2xl sm:border sm:border-border/55">
88
+ {selectedSlug ? (
89
+ <>
90
+ <MarketplaceDetailHeader onBack={() => setSelectedSlug(null)} />
91
+ <div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto bg-background">
92
+ <MarketplaceDetail
93
+ detail={detail.data}
94
+ error={detail.error instanceof Error ? detail.error : null}
95
+ installedPackage={detail.data ? installedById.get(detail.data.appId) : undefined}
96
+ isLoading={detail.isLoading}
97
+ isStarting={isStarting && detail.data?.install.spec === startingSource}
98
+ localeFallbacks={localeFallbacks}
99
+ onInstall={onInstall}
100
+ onUpdate={onUpdate}
101
+ operation={detail.data
102
+ ? findLatestAppPackageOperation(operations, detail.data.appId, detail.data.install.spec)
103
+ : undefined}
104
+ />
66
105
  </div>
67
- ) : null}
68
- <MarketplaceContent
69
- installedAppIds={installedAppIds}
70
- installedPackages={installedPackages}
71
- installingSource={installingSource}
106
+ </>
107
+ ) : (
108
+ <AppMarketplaceCatalog
109
+ error={error}
110
+ filter={filter}
111
+ installedById={installedById}
72
112
  isError={marketplace.isError}
113
+ isFetchingNextPage={marketplace.isFetchingNextPage}
73
114
  isLoading={marketplace.isLoading}
74
- isPending={isPending}
115
+ isStarting={isStarting}
75
116
  items={items}
76
117
  localeFallbacks={localeFallbacks}
118
+ onFilterChange={setFilter}
77
119
  onInstall={onInstall}
120
+ onLoadMore={() => void marketplace.fetchNextPage()}
121
+ onSearchChange={setSearch}
122
+ onSelect={setSelectedSlug}
123
+ onUpdate={onUpdate}
124
+ operations={operations}
125
+ hasNextPage={marketplace.hasNextPage}
126
+ search={search}
127
+ startingSource={startingSource}
78
128
  />
79
- </div>
80
- <div className="flex items-start gap-2 border-t border-border/70 bg-card px-5 py-3 text-[11px] leading-4 text-muted-foreground sm:px-6">
129
+ )}
130
+ <div className="flex items-start gap-2 border-t border-border/60 bg-muted/25 px-5 pb-[max(.75rem,env(safe-area-inset-bottom))] pt-3 text-[11px] leading-4 text-muted-foreground sm:px-6 sm:pb-3">
81
131
  <ShieldCheck className="mt-0.5 h-3.5 w-3.5 shrink-0" />
82
132
  <span>{t('appPackagesMarketplaceTrustHint')}</span>
83
133
  </div>
@@ -86,177 +136,17 @@ export function AppMarketplaceDialog({
86
136
  );
87
137
  }
88
138
 
89
- function MarketplaceHeader({
90
- onSearchChange,
91
- search,
92
- }: {
93
- onSearchChange: (value: string) => void;
94
- search: string;
95
- }) {
96
- return (
97
- <div className="border-b border-border/70 px-5 pb-4 pt-5 sm:px-6">
98
- <DialogHeader className="pr-8">
99
- <DialogTitle>{t('appPackagesMarketplaceTitle')}</DialogTitle>
100
- <DialogDescription>{t('appPackagesMarketplaceDescription')}</DialogDescription>
101
- </DialogHeader>
102
- <div className="relative mt-4">
103
- <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
104
- <Input
105
- autoFocus
106
- className="pl-9"
107
- value={search}
108
- onChange={(event) => onSearchChange(event.target.value)}
109
- placeholder={t('appPackagesMarketplaceSearch')}
110
- aria-label={t('appPackagesMarketplaceSearch')}
111
- />
112
- </div>
113
- </div>
114
- );
115
- }
116
-
117
- function MarketplaceContent({
118
- installedAppIds,
119
- installedPackages,
120
- installingSource,
121
- isError,
122
- isLoading,
123
- isPending,
124
- items,
125
- localeFallbacks,
126
- onInstall,
127
- }: {
128
- installedAppIds: Set<string>;
129
- installedPackages: AppPackageView[];
130
- installingSource?: string;
131
- isError: boolean;
132
- isLoading: boolean;
133
- isPending: boolean;
134
- items: ReturnType<typeof useAppMarketplace>['data'] extends infer Data
135
- ? Data extends { items: infer Items } ? Items : never
136
- : never;
137
- localeFallbacks: string[];
138
- onInstall: (source: string, registryUrl: string) => void;
139
- }) {
140
- if (isLoading) {
141
- return (
142
- <div className="space-y-2.5" aria-label={t('appPackagesMarketplaceLoading')}>
143
- <Skeleton className="h-32 w-full rounded-2xl" />
144
- <Skeleton className="h-32 w-full rounded-2xl" />
145
- </div>
146
- );
147
- }
148
- if (isError) {
149
- return (
150
- <div role="alert" className="rounded-xl border border-destructive/20 bg-destructive/5 px-4 py-4 text-sm text-destructive">
151
- {t('appPackagesMarketplaceFailed')}
152
- </div>
153
- );
154
- }
155
- if (items.length === 0) {
156
- return (
157
- <div className="flex flex-col items-center px-6 py-12 text-center">
158
- <Search className="h-6 w-6 text-muted-foreground" />
159
- <p className="mt-3 text-sm font-medium text-foreground">{t('appPackagesMarketplaceEmpty')}</p>
160
- </div>
161
- );
162
- }
163
-
139
+ function MarketplaceDetailHeader({ onBack }: { onBack: () => void }) {
164
140
  return (
165
- <div className="space-y-2.5">
166
- {items.map((item) => (
167
- <MarketplaceItemCard
168
- key={item.id}
169
- installed={installedAppIds.has(item.appId)}
170
- installedPackage={installedPackages.find((entry) => entry.id === item.appId)}
171
- isInstalling={isPending && installingSource === item.install.spec}
172
- isPending={isPending}
173
- item={item}
174
- localeFallbacks={localeFallbacks}
175
- onInstall={onInstall}
176
- />
177
- ))}
141
+ <div className="border-b border-border/60 bg-card px-4 pb-3 pt-[max(.75rem,env(safe-area-inset-top))] sm:px-5 sm:pt-3">
142
+ <button
143
+ type="button"
144
+ onClick={onBack}
145
+ className="inline-flex items-center gap-2 rounded-lg px-2 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
146
+ >
147
+ <ArrowLeft className="h-4 w-4" />
148
+ {t('appPackagesBackToMarketplace')}
149
+ </button>
178
150
  </div>
179
151
  );
180
152
  }
181
-
182
- function MarketplaceItemCard({
183
- installed,
184
- installedPackage,
185
- isInstalling,
186
- isPending,
187
- item,
188
- localeFallbacks,
189
- onInstall,
190
- }: {
191
- installed: boolean;
192
- installedPackage?: AppPackageView;
193
- isInstalling: boolean;
194
- isPending: boolean;
195
- item: NonNullable<ReturnType<typeof useAppMarketplace>['data']>['items'][number];
196
- localeFallbacks: string[];
197
- onInstall: (source: string, registryUrl: string) => void;
198
- }) {
199
- const displayName = installedPackage
200
- ? readLocalizedText(installedPackage.name, installedPackage.nameI18n)
201
- : item.name;
202
- const summary = pickLocalizedText(item.summaryI18n, item.summary, localeFallbacks);
203
- return (
204
- <article className="rounded-2xl border border-border/70 bg-card p-3.5 shadow-sm">
205
- <div className="flex items-start gap-3">
206
- <span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
207
- <AppWindow className="h-[18px] w-[18px]" />
208
- </span>
209
- <div className="min-w-0 flex-1">
210
- <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
211
- <h3 className="truncate text-sm font-semibold text-foreground">{displayName}</h3>
212
- {item.featured ? (
213
- <span className="rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-medium text-primary">
214
- {t('appPackagesMarketplaceFeatured')}
215
- </span>
216
- ) : null}
217
- </div>
218
- <p className="mt-1 line-clamp-2 text-xs leading-5 text-muted-foreground">{summary}</p>
219
- <div className="mt-2 flex flex-wrap items-center gap-1.5">
220
- <span className="text-[11px] text-muted-foreground/75">
221
- {item.publisher.name} · v{item.latestVersion}
222
- </span>
223
- {item.tags.slice(0, 3).map((tag) => (
224
- <span key={tag} className="rounded-full bg-muted px-2 py-0.5 text-[10px] text-muted-foreground">
225
- {tag}
226
- </span>
227
- ))}
228
- </div>
229
- </div>
230
- </div>
231
- <div className="mt-3 flex items-center justify-end gap-2 border-t border-border/60 pt-3">
232
- <a
233
- href={item.webUrl}
234
- target="_blank"
235
- rel="noreferrer"
236
- className="inline-flex h-8 items-center justify-center rounded-full border border-border bg-card px-3 text-xs font-medium text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
237
- >
238
- {t('appPackagesMarketplaceDetails')}
239
- <ExternalLink className="ml-1.5 h-3 w-3" />
240
- </a>
241
- <Button
242
- type="button"
243
- size="sm"
244
- disabled={installed || isPending}
245
- onClick={() => onInstall(item.install.spec, item.install.registry)}
246
- >
247
- {installed ? (
248
- <><Check className="mr-1.5 h-3.5 w-3.5" />{t('appPackagesMarketplaceInstalled')}</>
249
- ) : isInstalling ? t('appPackagesInstalling') : t('appPackagesInstall')}
250
- </Button>
251
- </div>
252
- </article>
253
- );
254
- }
255
-
256
- function readLocalizedText(
257
- fallback: string | undefined,
258
- localized: Record<string, string> | undefined,
259
- ): string {
260
- const languageTag = getLanguage() === 'zh' ? 'zh-CN' : 'en-US';
261
- return localized?.[languageTag] ?? fallback ?? '';
262
- }
@@ -0,0 +1,84 @@
1
+ import type { AppPackageOperationView } from '@nextclaw/client-sdk';
2
+ import { Check, LoaderCircle } from 'lucide-react';
3
+ import { Button } from '@/shared/components/ui/button';
4
+ import { t } from '@/shared/lib/i18n';
5
+ import { cn } from '@/shared/lib/utils';
6
+
7
+ export function MarketplaceInstallButton({
8
+ active,
9
+ canUpdate,
10
+ failed,
11
+ installed,
12
+ isStarting,
13
+ onAction,
14
+ operation,
15
+ }: {
16
+ active: boolean;
17
+ canUpdate: boolean;
18
+ failed: boolean;
19
+ installed: boolean;
20
+ isStarting: boolean;
21
+ onAction: () => void;
22
+ operation?: AppPackageOperationView;
23
+ }) {
24
+ return (
25
+ <Button
26
+ type="button"
27
+ size="sm"
28
+ variant={installed && !canUpdate || active ? 'secondary' : 'default'}
29
+ disabled={installed && !canUpdate || active || isStarting}
30
+ onClick={onAction}
31
+ className="shrink-0 rounded-full px-4"
32
+ >
33
+ {installed && !canUpdate ? (
34
+ <><Check className="mr-1.5 h-3.5 w-3.5" />{t('appPackagesMarketplaceInstalled')}</>
35
+ ) : active || isStarting ? (
36
+ <><LoaderCircle className="mr-1.5 h-3.5 w-3.5 animate-spin" />{operation ? operationStatusLabel(operation) : t('appPackagesPreparing')}</>
37
+ ) : canUpdate ? t('appPackagesUpdate') : failed ? t('appPackagesRetry') : t('appPackagesInstall')}
38
+ </Button>
39
+ );
40
+ }
41
+
42
+ export function OperationProgress({ operation }: { operation: AppPackageOperationView }) {
43
+ const progress = Math.max(4, Math.round((operation.completedSteps / operation.totalSteps) * 100));
44
+ const failed = operation.status === 'failed' || operation.status === 'interrupted';
45
+ return (
46
+ <div className="mt-2" role={failed ? 'alert' : 'status'} aria-live="polite">
47
+ <div className="h-1 overflow-hidden rounded-full bg-muted">
48
+ <div
49
+ className={cn('h-full rounded-full transition-[width] duration-300', failed ? 'bg-destructive' : 'bg-primary')}
50
+ style={{ width: failed ? '100%' : `${progress}%` }}
51
+ />
52
+ </div>
53
+ <p className={cn('mt-1.5 truncate text-[11px]', failed ? 'text-destructive' : 'text-muted-foreground')}>
54
+ {failed ? operation.error ?? t('appPackagesActionFailed') : operationStatusLabel(operation)}
55
+ </p>
56
+ </div>
57
+ );
58
+ }
59
+
60
+ export function findLatestAppPackageOperation(
61
+ operations: AppPackageOperationView[],
62
+ appId: string,
63
+ source: string,
64
+ ): AppPackageOperationView | undefined {
65
+ return operations.find((entry) => entry.appId === appId || entry.source === source);
66
+ }
67
+
68
+ function operationStatusLabel(operation: AppPackageOperationView): string {
69
+ switch (operation.status) {
70
+ case 'queued': return t('appPackagesPreparing');
71
+ case 'resolving': return t('appPackagesResolving');
72
+ case 'downloading': return t('appPackagesDownloading');
73
+ case 'verifying': return t('appPackagesVerifying');
74
+ case 'installing': return operation.action === 'rollback'
75
+ ? t('appPackagesSwitchingVersion')
76
+ : operation.action === 'uninstall'
77
+ ? t('appPackagesUninstalling')
78
+ : t('appPackagesInstalling');
79
+ case 'finalizing': return t('appPackagesFinalizing');
80
+ case 'succeeded': return t('appPackagesCompleted');
81
+ case 'failed':
82
+ case 'interrupted': return t('appPackagesActionFailed');
83
+ }
84
+ }