@jetcrab/pi-desk 1.1.1-dev → 1.1.5-dev

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 (117) hide show
  1. package/docs/pi-desk/README.md +47 -0
  2. package/docs/pi-desk/commands.md +76 -0
  3. package/docs/pi-desk/examples/local-application/browser/application.js +106 -0
  4. package/docs/pi-desk/examples/local-application/browser/entry.js +9 -0
  5. package/docs/pi-desk/examples/local-application/package.json +9 -0
  6. package/docs/pi-desk/examples/local-application/pi-desk.ts +11 -0
  7. package/docs/pi-desk/plugins.md +119 -0
  8. package/package.json +3 -2
  9. package/src/client/l2_biz/pi-settings/hooks/l2-use-pi-settings.ts +37 -21
  10. package/src/client/l2_biz/pi-settings/l2-pi-settings.tsx +37 -43
  11. package/src/client/l2_biz/pi-settings/views/l2-mcp-settings-editor.tsx +3 -1
  12. package/src/client/l2_biz/pi-settings/views/l2-mcp-settings-list.tsx +1 -1
  13. package/src/client/l2_biz/plugin-host/hooks/l2-use-plugin-catalog.ts +3 -1
  14. package/src/client/l2_biz/plugin-host/hooks/l2-use-plugin-download-source.ts +5 -1
  15. package/src/client/l2_biz/plugin-host/l2-plugin-capability-details.tsx +57 -41
  16. package/src/client/l2_biz/plugin-host/l2-plugin-management-controls.tsx +89 -116
  17. package/src/client/l2_biz/plugin-host/l2-plugin-management.tsx +492 -338
  18. package/src/client/l2_biz/plugin-host/l2-use-plugin-management.ts +72 -22
  19. package/src/client/l2_biz/plugin-host/views/l2-plugin-catalog-detail.tsx +104 -76
  20. package/src/client/l2_biz/plugin-host/views/l2-plugin-catalog-search.tsx +101 -56
  21. package/src/client/l2_biz/plugin-host/views/l2-plugin-direct-install.tsx +91 -59
  22. package/src/client/l2_biz/plugin-host/views/l2-plugin-download-source.tsx +36 -27
  23. package/src/client/l2_biz/plugin-host/views/l2-plugin-installed-list.tsx +218 -208
  24. package/src/client/l2_biz/plugin-host/views/l2-plugin-management-channel.tsx +12 -5
  25. package/src/client/l2_biz/plugin-host/views/l2-plugin-management-dialog.tsx +84 -106
  26. package/src/client/l2_biz/plugin-host/views/l2-plugin-readme.tsx +29 -15
  27. package/src/client/l2_biz/settings/hooks/l2-use-project-model-default-settings.ts +22 -11
  28. package/src/client/l2_biz/settings/l2-settings.tsx +6 -11
  29. package/src/client/l2_biz/settings/views/l2-project-model-default-settings.tsx +20 -14
  30. package/src/client/l2_biz/terminal/l2-terminal-workspace.tsx +35 -1
  31. package/src/client/l2_biz/workbench/file-workspace/l2-work-session-file-preview.tsx +6 -2
  32. package/src/client/l2_biz/workbench/hooks/l2-workbench-chat-viewport.ts +14 -1
  33. package/src/client/l2_biz/workbench/l2-workbench-branch-picker.tsx +33 -5
  34. package/src/client/l2_biz/workbench/l2-workbench-sidebar-layout.tsx +3 -1
  35. package/src/client/l2_biz/workbench/l2-workbench.tsx +20 -3
  36. package/src/client/l3_modules/conversation/l3-conversation-call-summary.tsx +0 -3
  37. package/src/client/l3_modules/project-files/l3-file-context-menu-content.tsx +1 -0
  38. package/src/client/l4_foundation/ui/l4-app-dialog.tsx +3 -1
  39. package/src/client/l4_foundation/ui/l4-image-viewer.tsx +21 -0
  40. package/src/client/l4_foundation/ui/shadcn/checkbox.tsx +27 -0
  41. package/src/client/l4_foundation/ui/shadcn/context-menu.tsx +8 -1
  42. package/src/client/l4_foundation/ui/shadcn/dropdown-menu.tsx +5 -0
  43. package/src/client/l4_foundation/ui/shadcn/popover.tsx +2 -0
  44. package/src/client/l4_foundation/ui/shadcn/select.tsx +2 -0
  45. package/src/client/l4_foundation/ui/shadcn/switch.tsx +31 -0
  46. package/src/client/l4_foundation/ui/shadcn/tabs.tsx +74 -0
  47. package/src/common/l2_biz/plugin/l2-plugin-management-messages.ts +52 -22
  48. package/src/common/l3_modules/conversation/l3-conversation-locale-messages.ts +2 -2
  49. package/src/common/l3_modules/conversation/l3-conversation-pidesk.ts +1 -1
  50. package/src/common/l4_foundation/plugin/l4-plugin-package.ts +1 -0
  51. package/src/server/l2_biz/plugin-management/l2-plugin-catalog.ts +16 -5
  52. package/src/server/l2_biz/plugin-management/l2-plugin-commands.ts +21 -1
  53. package/src/server/l2_biz/plugin-management/l2-plugin-management.ts +21 -2
  54. package/src/server/l2_biz/plugin-management/l2-plugin-updates.ts +17 -3
  55. package/src/server/l4_foundation/pi/l4-pi-basic-session-services.ts +3 -3
  56. package/src/server/l4_foundation/pi/l4-pi-package-maintenance-runner.mts +4 -11
  57. package/src/server/l4_foundation/pi/l4-pi-plugin-registry-config.ts +52 -34
  58. package/src/server/l4_foundation/pi/l4-pi-plugin-registry-network.ts +5 -3
  59. package/src/server/l4_foundation/pi/l4-pi-plugin-registry.ts +23 -8
  60. package/src/server/l4_foundation/pi/l4-pi-session-initialization.ts +2 -5
  61. package/src/server/l4_foundation/pidesk/l4-pidesk-command.ts +8 -1
  62. package/src/server/l4_foundation/pidesk/l4-pidesk-runtime.ts +24 -38
  63. package/src/server/l4_foundation/pidesk/l4-pidesk-system-prompt.ts +11 -0
  64. package/temp/build/pi-desk/release/.next/BUILD_ID +1 -1
  65. package/temp/build/pi-desk/release/.next/build-manifest.json +3 -3
  66. package/temp/build/pi-desk/release/.next/react-loadable-manifest.json +1 -1
  67. package/temp/build/pi-desk/release/.next/required-server-files.js +2 -2
  68. package/temp/build/pi-desk/release/.next/required-server-files.json +2 -2
  69. package/temp/build/pi-desk/release/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  70. package/temp/build/pi-desk/release/.next/server/app/_global-error.html +1 -1
  71. package/temp/build/pi-desk/release/.next/server/app/_global-error.rsc +3 -3
  72. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_full.segment.rsc +3 -3
  73. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +3 -3
  74. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  75. package/temp/build/pi-desk/release/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  76. package/temp/build/pi-desk/release/.next/server/app/api/health/route.js +1 -1
  77. package/temp/build/pi-desk/release/.next/server/app/login/page.js +1 -1
  78. package/temp/build/pi-desk/release/.next/server/app/login/page_client-reference-manifest.js +1 -1
  79. package/temp/build/pi-desk/release/.next/server/app/page.js +79 -79
  80. package/temp/build/pi-desk/release/.next/server/app/page_client-reference-manifest.js +1 -1
  81. package/temp/build/pi-desk/release/.next/server/chunks/25406.js +8 -77
  82. package/temp/build/pi-desk/release/.next/server/chunks/{81107.js → 35348.js} +2 -2
  83. package/temp/build/pi-desk/release/.next/server/chunks/35985.js +4 -4
  84. package/temp/build/pi-desk/release/.next/server/chunks/36361.js +1 -1
  85. package/temp/build/pi-desk/release/.next/server/chunks/47757.js +6 -75
  86. package/temp/build/pi-desk/release/.next/server/chunks/50520.js +4 -4
  87. package/temp/build/pi-desk/release/.next/server/chunks/58330.js +1 -1
  88. package/temp/build/pi-desk/release/.next/server/chunks/84231.js +2 -2
  89. package/temp/build/pi-desk/release/.next/server/chunks/89236.js +2 -2
  90. package/temp/build/pi-desk/release/.next/server/chunks/92386.js +5 -5
  91. package/temp/build/pi-desk/release/.next/server/chunks/94518.js +1 -1
  92. package/temp/build/pi-desk/release/.next/server/chunks/9874.js +1 -1
  93. package/temp/build/pi-desk/release/.next/server/middleware-build-manifest.js +1 -1
  94. package/temp/build/pi-desk/release/.next/server/middleware-react-loadable-manifest.js +1 -1
  95. package/temp/build/pi-desk/release/.next/server/pages/500.html +1 -1
  96. package/temp/build/pi-desk/release/.next/static/chunks/14197-3d3ddd426ac06c9c.js +3 -0
  97. package/temp/build/pi-desk/release/.next/static/chunks/34903-e4205c0becabea76.js +318 -0
  98. package/temp/build/pi-desk/release/.next/static/chunks/{54206.7c0792096df8a5cf.js → 54206.82a63ab971911eda.js} +3 -3
  99. package/temp/build/pi-desk/release/.next/static/chunks/74132-876f256c7a6101af.js +13 -0
  100. package/temp/build/pi-desk/release/.next/static/chunks/78923-17a9c337b29dcecd.js +1 -0
  101. package/temp/build/pi-desk/release/.next/static/chunks/96995-8f8313ea2ac3abd7.js +62 -0
  102. package/temp/build/pi-desk/release/.next/static/chunks/app/{layout-7c5409cced9c2501.js → layout-4faf3a52d219296d.js} +2 -2
  103. package/temp/build/pi-desk/release/.next/static/chunks/app/login/{page-8c87fb9e6c918848.js → page-bf893718bb151f3c.js} +1 -1
  104. package/temp/build/pi-desk/release/.next/static/chunks/app/page-28e1d77d6b884657.js +158 -0
  105. package/temp/build/pi-desk/release/.next/static/chunks/{webpack-45424d28099bbefa.js → webpack-7bfbf627379fb4c1.js} +1 -1
  106. package/temp/build/pi-desk/release/.next/static/css/3213ee28336bddc2.css +1 -0
  107. package/temp/build/pi-desk/release/.next/trace-build +1 -1
  108. package/src/server/l4_foundation/pidesk/l4-pidesk-skill-template.ts +0 -87
  109. package/temp/build/pi-desk/release/.next/static/chunks/27491-8b2d3e345b9142ea.js +0 -318
  110. package/temp/build/pi-desk/release/.next/static/chunks/7089-600a8f5593b7846c.js +0 -3
  111. package/temp/build/pi-desk/release/.next/static/chunks/74132-0884df6a6f11861a.js +0 -13
  112. package/temp/build/pi-desk/release/.next/static/chunks/78923-539a8b5f7af3c54a.js +0 -1
  113. package/temp/build/pi-desk/release/.next/static/chunks/96995-ae32167974365050.js +0 -62
  114. package/temp/build/pi-desk/release/.next/static/chunks/app/page-a85b2f17c68cff60.js +0 -158
  115. package/temp/build/pi-desk/release/.next/static/css/56486914a8ba0a1b.css +0 -1
  116. /package/temp/build/pi-desk/release/.next/static/{AIPx4Oo1zqM8gixtv1I69 → IgHJ40fniC07-ioYUKobL}/_buildManifest.js +0 -0
  117. /package/temp/build/pi-desk/release/.next/static/{AIPx4Oo1zqM8gixtv1I69 → IgHJ40fniC07-ioYUKobL}/_ssgManifest.js +0 -0
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState } from 'react'
4
- import { ChevronDownIcon } from 'lucide-react'
3
+ import { useEffect, useRef, useState } from 'react'
4
+ import { ArrowLeftIcon, ChevronDownIcon } from 'lucide-react'
5
5
  import { useTranslation } from 'react-i18next'
6
6
  import { selectL4LocalizedText } from '@common/l4_foundation/locale/l4-localized-text'
7
7
  import { useL4Region } from '@client/l4_foundation/locale/l4-region-provider'
@@ -9,7 +9,17 @@ import type { L2PluginCatalogItem } from '@common/l2_biz/plugin/l2-plugin-catalo
9
9
  import type { L2PluginManagementSnapshot } from '@common/l2_biz/plugin/l2-plugin-management-contract'
10
10
  import { Button } from '@client/l4_foundation/ui/shadcn/button'
11
11
  import { Popover, PopoverContent, PopoverTrigger } from '@client/l4_foundation/ui/shadcn/popover'
12
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@client/l4_foundation/ui/shadcn/tabs'
13
+ import {
14
+ Collapsible,
15
+ CollapsibleContent,
16
+ CollapsibleTrigger
17
+ } from '@client/l4_foundation/ui/shadcn/collapsible'
12
18
  import { L2PluginCapabilityDetails } from './l2-plugin-capability-details'
19
+ import {
20
+ L2PluginManagementControls,
21
+ L2PluginManagementFeedback
22
+ } from './l2-plugin-management-controls'
13
23
  import type { L2PluginManagementBiz } from './l2-plugin-management-biz'
14
24
  import {
15
25
  pluginDisplayName,
@@ -21,7 +31,7 @@ import { useL2PluginCatalog } from './hooks/l2-use-plugin-catalog'
21
31
  import { useL2PluginDownloadSource } from './hooks/l2-use-plugin-download-source'
22
32
  import { useL2PluginDirectInstall } from './hooks/l2-use-plugin-direct-install'
23
33
  import { L2PluginDownloadSourceSelector } from './views/l2-plugin-download-source'
24
- import { L2PluginInstalledList } from './views/l2-plugin-installed-list'
34
+ import { L2PluginInstalledList, type L2PluginStatusFilter } from './views/l2-plugin-installed-list'
25
35
  import { L2PluginCatalogSearch } from './views/l2-plugin-catalog-search'
26
36
  import { L2PluginCatalogDetailView } from './views/l2-plugin-catalog-detail'
27
37
  import { L2PluginManagementChannel } from './views/l2-plugin-management-channel'
@@ -46,9 +56,10 @@ export function L2PluginManagement({
46
56
  }: L2PluginManagementProps): React.JSX.Element {
47
57
  const { t } = useTranslation('pluginManagement')
48
58
  const { locale } = useL4Region()
49
- const [tab, setTab] = useState<'installed' | 'search'>('installed')
59
+ const [tab, setTab] = useState('installed')
50
60
  const [filter, setFilter] = useState('')
51
- const [channel, setChannel] = useState('')
61
+ const [statusFilter, setStatusFilter] = useState<L2PluginStatusFilter>('all')
62
+ const [batchMode, setBatchMode] = useState(false)
52
63
  const [selected, setSelected] = useState<ReadonlySet<string>>(() => new Set())
53
64
  useEffect(() => {
54
65
  if (!snapshot) return
@@ -60,24 +71,17 @@ export function L2PluginManagement({
60
71
  }, [snapshot, selected])
61
72
  const [catalogTag, setCatalogTag] = useState('')
62
73
  const [catalogVersionExplicit, setCatalogVersionExplicit] = useState(false)
63
- const management = useL2PluginManagement({ biz, snapshot, onSnapshot, onRestartScheduled })
64
- const selectedSources =
65
- snapshot?.plugins
66
- .filter((plugin) => selected.has(pluginRequestKey(plugin.source)))
67
- .map((plugin) => plugin.source) ?? []
68
- const selectedNpmSources = selectedSources.filter((source) => source.startsWith('npm:'))
69
- const selectSources = (keys: string[], checked: boolean): void => {
70
- setSelected((current) => {
71
- const next = new Set(current)
72
- keys.forEach((key) => {
73
- if (checked) next.add(key)
74
- else next.delete(key)
75
- })
76
- return next
77
- })
78
- }
74
+ const management = useL2PluginManagement({
75
+ biz,
76
+ snapshot,
77
+ onSnapshot,
78
+ onRestartScheduled,
79
+ basicMode
80
+ })
79
81
  const catalog = useL2PluginCatalog(biz, tab === 'search')
80
- const download = useL2PluginDownloadSource(biz)
82
+ const download = useL2PluginDownloadSource(biz, () => {
83
+ if (!basicMode) void management.refresh()
84
+ })
81
85
  const direct = useL2PluginDirectInstall(biz, snapshot?.plugins)
82
86
  const disabled = basicMode || management.requests.has('reload')
83
87
  const browserErrors = new Map(
@@ -104,19 +108,17 @@ export function L2PluginManagement({
104
108
  const directPlugin = snapshot?.plugins.find(
105
109
  (plugin) => pluginRequestKey(plugin.source) === directKey
106
110
  )
107
- const catalogPending =
108
- management.requests.has(catalogKey) ||
109
- Boolean(catalogPlugin?.operation && catalogPlugin.operation.phase !== 'failed')
110
- const directPending =
111
- direct.sources.length > 0 &&
112
- direct.sources.every((source) => {
113
- const key = pluginRequestKey(source)
114
- const plugin = snapshot?.plugins.find((item) => pluginRequestKey(item.source) === key)
115
- return (
116
- management.requests.has(key) ||
117
- Boolean(plugin?.operation && plugin.operation.phase !== 'failed')
118
- )
119
- })
111
+ const busy = (source: string): boolean => {
112
+ const plugin = snapshot?.plugins.find(
113
+ (item) => pluginRequestKey(item.source) === pluginRequestKey(source)
114
+ )
115
+ return (
116
+ management.requests.has(pluginRequestKey(source)) ||
117
+ Boolean(plugin?.operation && plugin.operation.phase !== 'failed')
118
+ )
119
+ }
120
+ const catalogPending = busy(`npm:${catalog.selection?.name ?? ''}`)
121
+ const directPending = direct.sources.some(busy)
120
122
  const directErrors = direct.sources
121
123
  .map((source) => {
122
124
  const key = pluginRequestKey(source)
@@ -128,79 +130,133 @@ export function L2PluginManagement({
128
130
  })
129
131
  .filter(Boolean)
130
132
  .join('\n')
133
+ const detailOpen = Boolean(management.detailSource || catalog.selection)
134
+ const backButton = useRef<HTMLButtonElement>(null)
135
+ const returnFocus = useRef<HTMLElement | null>(null)
136
+ useEffect(() => {
137
+ if (detailOpen) backButton.current?.focus()
138
+ else if (returnFocus.current?.isConnected) {
139
+ returnFocus.current.focus()
140
+ returnFocus.current = null
141
+ }
142
+ }, [detailOpen])
143
+ const rememberFocus = (): void => {
144
+ returnFocus.current =
145
+ document.activeElement instanceof HTMLElement ? document.activeElement : null
146
+ }
147
+ const closeDetail = (): void => {
148
+ management.closeDetail()
149
+ catalog.closeDetail()
150
+ }
131
151
  const catalogTarget = (item: L2PluginCatalogItem): L2PluginCatalogItem => {
132
152
  const installed = snapshot?.plugins.find(
133
153
  (plugin) => pluginNpmSpec(plugin.source)?.name === item.name
134
154
  )
135
- const tag = catalogTag || installed?.updateTag
136
- return tag ? { ...item, version: tag } : item
155
+ return { ...item, version: installed?.updateTag ?? 'latest' }
137
156
  }
138
157
  const openCatalog = (item: L2PluginCatalogItem): void => {
158
+ rememberFocus()
139
159
  management.closeDetail()
160
+ setCatalogTag('')
140
161
  setCatalogVersionExplicit(false)
141
162
  catalog.openDetail(catalogTarget(item))
142
163
  }
143
164
  const openInstalled = (source: string): void => {
165
+ rememberFocus()
144
166
  catalog.closeDetail()
145
167
  management.openDetail(source)
146
168
  }
147
169
  const installCatalogItem = (item: L2PluginCatalogItem): void => {
148
170
  void catalog.prepareInstall(catalogTarget(item), (detail) =>
149
- management.requestInstall(
150
- {
151
- source: `npm:${detail.name}`,
152
- registry: detail.registry,
153
- ...(catalogTag ? { tag: catalogTag } : {})
154
- },
155
- detail
156
- )
171
+ management.requestInstall({ source: `npm:${detail.name}`, registry: detail.registry }, detail)
157
172
  )
158
173
  }
159
- const closeDialog = (): void => {
160
- if (management.confirmation) management.setConfirmation(null)
161
- else {
162
- management.closeDetail()
163
- catalog.closeDetail()
164
- }
174
+ const selectSources = (keys: string[], checked: boolean): void => {
175
+ setSelected((current) => {
176
+ const next = new Set(current)
177
+ keys.forEach((key) => {
178
+ if (checked) next.add(key)
179
+ else next.delete(key)
180
+ })
181
+ return next
182
+ })
165
183
  }
184
+ const updateSources = (onlySelected: boolean): string[] =>
185
+ snapshot?.plugins
186
+ .filter(
187
+ (plugin) =>
188
+ plugin.kind !== 'extension' &&
189
+ plugin.updateAvailable === true &&
190
+ !busy(plugin.source) &&
191
+ (!onlySelected || selected.has(pluginRequestKey(plugin.source)))
192
+ )
193
+ .map((plugin) => plugin.source) ?? []
166
194
  const operationMessage = (phase?: string): string | null => (phase ? t(`phase_${phase}`) : null)
195
+ const openDirect = (): void => {
196
+ setTab('installed')
197
+ direct.setOpen(true)
198
+ }
167
199
  return (
168
200
  <div className="flex h-full min-h-0 min-w-0 flex-col">
169
201
  <header className="shrink-0 space-y-3 border-b p-4 sm:px-6">
202
+ {detailOpen ? (
203
+ <Button
204
+ ref={backButton}
205
+ size="sm"
206
+ variant="ghost"
207
+ className="-ml-2"
208
+ onClick={closeDetail}
209
+ >
210
+ <ArrowLeftIcon />
211
+ {t('backToList')}
212
+ </Button>
213
+ ) : null}
170
214
  <div className="flex flex-wrap items-center justify-between gap-2">
171
- <h2 className="text-xl font-semibold">{t('title')}</h2>
215
+ <h2 className="min-w-0 text-xl font-semibold [overflow-wrap:anywhere]">
216
+ {detailOpen
217
+ ? selectedPlugin
218
+ ? pluginDisplayName(selectedPlugin)
219
+ : (catalog.selection?.name ?? t('details'))
220
+ : t('title')}
221
+ </h2>
172
222
  <div className="flex flex-wrap items-center gap-2">
173
- <Popover>
174
- <PopoverTrigger render={<Button size="sm" variant="outline" />}>
175
- {t('downloadSource')}:
176
- {download.loading && !download.settings
177
- ? t('loading')
178
- : t(`download_${download.settings?.downloadSource.mode ?? download.mode}`)}
179
- <ChevronDownIcon className="size-4" />
180
- </PopoverTrigger>
181
- <PopoverContent align="end" positionerClassName="z-[150]">
182
- <L2PluginDownloadSourceSelector
183
- id="plugin-default-download"
184
- mode={download.mode}
185
- registry={download.registry}
186
- savedRegistry={
187
- download.settings?.downloadSource.mode === 'custom'
188
- ? download.settings.downloadSource.registry
189
- : undefined
190
- }
191
- recommendedRegistry={download.settings?.recommendedRegistry}
192
- loading={download.loading}
193
- saving={download.saving}
194
- error={download.error}
195
- onMode={(mode) => {
196
- if (mode !== 'default') download.changeMode(mode)
197
- }}
198
- onRegistry={download.setRegistry}
199
- onSave={() => void download.save()}
200
- onRetry={() => void download.retry()}
201
- />
202
- </PopoverContent>
203
- </Popover>
223
+ {!detailOpen ? (
224
+ <Popover>
225
+ <PopoverTrigger render={<Button size="sm" variant="ghost" />}>
226
+ {t('downloadSource')}:
227
+ {download.loading && !download.settings
228
+ ? t('loading')
229
+ : t(`download_${download.settings?.downloadSource.mode ?? download.mode}`)}
230
+ <ChevronDownIcon className="size-4" />
231
+ </PopoverTrigger>
232
+ <PopoverContent
233
+ align="end"
234
+ positionerClassName="z-[150]"
235
+ className="w-80 max-w-[calc(100vw-2rem)]"
236
+ >
237
+ <L2PluginDownloadSourceSelector
238
+ id="plugin-default-download"
239
+ mode={download.mode}
240
+ registry={download.registry}
241
+ savedRegistry={
242
+ download.settings?.downloadSource.mode === 'custom'
243
+ ? download.settings.downloadSource.registry
244
+ : undefined
245
+ }
246
+ recommendedRegistry={download.settings?.recommendedRegistry}
247
+ loading={download.loading}
248
+ saving={download.saving}
249
+ error={download.error}
250
+ onMode={(mode) => {
251
+ if (mode !== 'default') download.changeMode(mode)
252
+ }}
253
+ onRegistry={download.setRegistry}
254
+ onSave={() => void download.save()}
255
+ onRetry={() => void download.retry()}
256
+ />
257
+ </PopoverContent>
258
+ </Popover>
259
+ ) : null}
204
260
  {basicMode || snapshot?.restartRequired ? (
205
261
  <Button
206
262
  size="sm"
@@ -256,218 +312,333 @@ export function L2PluginManagement({
256
312
  </p>
257
313
  ) : null}
258
314
  </header>
259
- <div
260
- role="tablist"
261
- aria-label={t('title')}
262
- className="flex shrink-0 gap-1 border-b px-4 pt-2 sm:px-6"
315
+ <Tabs
316
+ value={tab}
317
+ onValueChange={(value) => setTab(String(value))}
318
+ className={`min-h-0 flex-1 gap-0 ${detailOpen ? 'hidden' : ''}`}
263
319
  >
264
- {(['installed', 'search'] as const).map((value) => (
265
- <button
266
- key={value}
267
- id={`plugin-tab-${value}`}
268
- type="button"
269
- role="tab"
270
- aria-selected={tab === value}
271
- aria-controls={`plugin-panel-${value}`}
272
- tabIndex={tab === value ? 0 : -1}
273
- className={`min-h-9 border-b-2 px-3 text-sm font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring ${tab === value ? 'border-primary text-foreground' : 'border-transparent text-muted-foreground hover:bg-accent'}`}
274
- onClick={() => setTab(value)}
275
- onKeyDown={(event) => {
276
- if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
277
- event.preventDefault()
278
- const next =
279
- event.key === 'Home'
280
- ? 'installed'
281
- : event.key === 'End'
282
- ? 'search'
283
- : tab === 'installed'
284
- ? 'search'
285
- : 'installed'
286
- setTab(next)
287
- document.getElementById(`plugin-tab-${next}`)?.focus()
288
- }
289
- }}
290
- >
291
- {t(value === 'installed' ? 'installedTab' : 'searchPlugins')}
292
- </button>
293
- ))}
294
- </div>
295
- <div
296
- id="plugin-panel-installed"
297
- role="tabpanel"
298
- aria-labelledby="plugin-tab-installed"
299
- tabIndex={0}
300
- hidden={tab !== 'installed'}
301
- className={`pi-desk-chat-scrollbar min-h-0 flex-1 overflow-y-auto p-4 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring sm:p-6 ${tab !== 'installed' ? 'hidden' : ''}`}
302
- >
303
- <div className="space-y-4">
304
- {direct.open ? (
305
- <L2PluginDirectInstall
306
- source={direct.source}
307
- isNpm={Boolean(direct.spec)}
308
- multiple={direct.sources.length > 1}
309
- tag={direct.tag}
310
- selectedVersion={direct.selectedVersion}
311
- onTag={direct.setTag}
312
- detail={direct.detail}
313
- loading={direct.loading}
314
- pending={directPending}
315
- disabled={disabled}
316
- error={
317
- direct.error ??
318
- (directErrors || null) ??
319
- (directPlugin?.operation?.phase === 'failed'
320
- ? directPlugin.operation.message
321
- : null)
320
+ <div className="shrink-0 border-b px-4 pt-2 sm:px-6">
321
+ <TabsList variant="line" aria-label={t('title')} className="h-9">
322
+ <TabsTrigger value="installed" className="px-3">
323
+ {t('installedTab')}
324
+ {snapshot ? (
325
+ <span className="text-xs text-muted-foreground">{snapshot.plugins.length}</span>
326
+ ) : null}
327
+ </TabsTrigger>
328
+ <TabsTrigger value="search" className="px-3">
329
+ {t('searchPlugins')}
330
+ </TabsTrigger>
331
+ </TabsList>
332
+ </div>
333
+ <TabsContent
334
+ value="installed"
335
+ keepMounted
336
+ className="pi-desk-chat-scrollbar min-h-0 overflow-y-auto p-4 data-[hidden]:hidden sm:p-6"
337
+ >
338
+ <div className="space-y-4">
339
+ {direct.open ? (
340
+ <L2PluginDirectInstall
341
+ source={direct.source}
342
+ isNpm={Boolean(direct.spec)}
343
+ multiple={direct.sources.length > 1}
344
+ tag={direct.tag}
345
+ selectedVersion={direct.selectedVersion}
346
+ onTag={direct.setTag}
347
+ detail={direct.detail}
348
+ loading={direct.loading}
349
+ pending={directPending}
350
+ disabled={disabled}
351
+ error={direct.error ?? (directErrors || null)}
352
+ operationMessage={
353
+ directPending
354
+ ? (operationMessage(directPlugin?.operation?.phase) ?? t('submitting'))
355
+ : null
356
+ }
357
+ override={direct.override}
358
+ registry={direct.registry}
359
+ onSource={direct.changeSource}
360
+ onMode={direct.setOverride}
361
+ onRegistry={direct.setRegistry}
362
+ onRead={() => void direct.loadDetail()}
363
+ onVersion={(version) => void direct.loadDetail(version)}
364
+ onInstall={() =>
365
+ void direct.install(management.requestInstall, (items) =>
366
+ management.setConfirmation({ kind: 'batch', input: { action: 'add', items } })
367
+ )
368
+ }
369
+ onClose={() => direct.setOpen(false)}
370
+ />
371
+ ) : null}
372
+ <L2PluginInstalledList
373
+ snapshot={snapshot}
374
+ loading={management.loading}
375
+ filter={filter}
376
+ statusFilter={statusFilter}
377
+ batchMode={batchMode}
378
+ selected={selected}
379
+ onStatusFilter={setStatusFilter}
380
+ onBatchMode={(value) => {
381
+ setBatchMode(value)
382
+ if (!value) setSelected(new Set())
383
+ }}
384
+ onSelect={selectSources}
385
+ onUpdateAll={() =>
386
+ void management.executeBatch({ action: 'update', sources: updateSources(false) })
322
387
  }
323
- operationMessage={
324
- directPending
325
- ? (operationMessage(directPlugin?.operation?.phase) ?? t('submitting'))
326
- : null
388
+ onUpdateSelected={() =>
389
+ void management.executeBatch({ action: 'update', sources: updateSources(true) })
327
390
  }
328
- override={direct.override}
329
- registry={direct.registry}
330
- onSource={direct.changeSource}
331
- onMode={direct.setOverride}
332
- onRegistry={direct.setRegistry}
333
- onRead={() => void direct.loadDetail()}
334
- onVersion={(version) => void direct.loadDetail(version)}
335
- onInstall={() =>
336
- void direct.install(management.requestInstall, (items) =>
337
- management.setConfirmation({ kind: 'batch', input: { action: 'add', items } })
338
- )
391
+ onRemoveSelected={() =>
392
+ management.setConfirmation({
393
+ kind: 'batch',
394
+ input: {
395
+ action: 'del',
396
+ sources:
397
+ snapshot?.plugins
398
+ .filter(
399
+ (plugin) =>
400
+ selected.has(pluginRequestKey(plugin.source)) && !busy(plugin.source)
401
+ )
402
+ .map((plugin) => plugin.source) ?? []
403
+ }
404
+ })
339
405
  }
340
- onClose={() => direct.setOpen(false)}
406
+ disabled={disabled}
407
+ requests={management.requests}
408
+ errors={management.errors}
409
+ browserErrors={browserErrors}
410
+ onFilter={setFilter}
411
+ onDetail={openInstalled}
412
+ onAction={management.handleControlAction}
413
+ onRefresh={() => void management.refresh()}
341
414
  />
342
- ) : null}
343
- <L2PluginInstalledList
344
- snapshot={snapshot}
345
- loading={management.loading}
346
- filter={filter}
347
- channel={channel}
348
- selected={selected}
349
- onChannel={setChannel}
350
- onSelect={selectSources}
351
- onCheckSelected={() => void management.refresh(selectedNpmSources)}
352
- onUpdateSelected={() =>
353
- void management.executeBatch({ action: 'update', sources: selectedSources })
354
- }
355
- onRemoveSelected={() =>
356
- management.setConfirmation({
357
- kind: 'batch',
358
- input: { action: 'del', sources: selectedSources }
359
- })
360
- }
361
- onTagSelected={(tag) =>
362
- void management.executeBatch({ action: 'tag', sources: selectedNpmSources, tag })
363
- }
415
+ {!snapshot?.plugins.length && !direct.open ? (
416
+ <Button size="sm" variant="outline" disabled={disabled} onClick={openDirect}>
417
+ {t('directInstall')}
418
+ </Button>
419
+ ) : null}
420
+ </div>
421
+ </TabsContent>
422
+ <TabsContent
423
+ value="search"
424
+ keepMounted
425
+ className="pi-desk-chat-scrollbar min-h-0 overflow-y-auto p-4 data-[hidden]:hidden sm:p-6"
426
+ >
427
+ <L2PluginCatalogSearch
428
+ query={catalog.query}
429
+ kind={catalog.kind}
430
+ searchSource={catalog.searchSource}
431
+ registry={catalog.registry}
432
+ items={catalog.items}
433
+ loading={catalog.loading}
434
+ error={catalog.error}
435
+ searched={catalog.searched}
436
+ hasMore={catalog.hasMore}
364
437
  disabled={disabled}
438
+ snapshot={snapshot}
365
439
  requests={management.requests}
440
+ preparing={catalog.preparing}
366
441
  errors={management.errors}
367
- browserErrors={browserErrors}
368
- onFilter={setFilter}
369
- onDetail={openInstalled}
370
- onAction={management.handleControlAction}
371
- onDirectInstall={() => direct.setOpen(true)}
372
- onRefresh={() => void management.refresh(undefined, channel || undefined)}
373
- />
374
- </div>
375
- </div>
376
- <div
377
- id="plugin-panel-search"
378
- role="tabpanel"
379
- aria-labelledby="plugin-tab-search"
380
- tabIndex={0}
381
- hidden={tab !== 'search'}
382
- className={`pi-desk-chat-scrollbar min-h-0 flex-1 overflow-y-auto p-4 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring sm:p-6 ${tab !== 'search' ? 'hidden' : ''}`}
383
- >
384
- <L2PluginCatalogSearch
385
- query={catalog.query}
386
- kind={catalog.kind}
387
- searchSource={catalog.searchSource}
388
- registry={catalog.registry}
389
- items={catalog.items}
390
- loading={catalog.loading}
391
- error={catalog.error}
392
- searched={catalog.searched}
393
- hasMore={catalog.hasMore}
394
- disabled={disabled}
395
- snapshot={snapshot}
396
- requests={management.requests}
397
- preparing={catalog.preparing}
398
- errors={management.errors}
399
- installErrors={catalog.installErrors}
400
- onQuery={catalog.setQuery}
401
- onCompositionStart={catalog.beginComposition}
402
- onCompositionEnd={catalog.endComposition}
403
- onKind={catalog.setKind}
404
- onSearchSource={catalog.setSearchSource}
405
- onRegistry={catalog.setRegistry}
406
- onDetail={openCatalog}
407
- onInstall={installCatalogItem}
408
- onUpdate={(source) => void management.execute({ kind: 'update', source })}
409
- onMore={catalog.loadMore}
410
- onRetry={catalog.retry}
411
- />
412
- </div>
413
- <L2PluginManagementDialog
414
- open={Boolean(management.confirmation || management.detailSource || catalog.selection)}
415
- title={
416
- selectedPlugin
417
- ? pluginDisplayName(selectedPlugin)
418
- : (catalog.selection?.name ?? t('details'))
419
- }
420
- confirmation={management.confirmation}
421
- onClose={closeDialog}
422
- onConfirm={(action) => void management.execute(action)}
423
- >
424
- {selectedPlugin ? (
425
- <div className="min-w-0 space-y-5">
426
- {management.detail?.readme ? (
427
- <L2PluginReadme key={selectedPlugin.source} readme={management.detail.readme} />
428
- ) : null}
429
- <L2PluginCapabilityDetails
430
- plugin={selectedPlugin}
431
- detail={management.detail}
432
- detailLoading={management.detailLoading}
433
- detailError={management.detailError}
434
- browserContributions={
435
- selectedPlugin.pluginName
436
- ? (management.pluginHost
437
- .getRegisteredBrowserDescriptors()
438
- .find((descriptor) => descriptor.pluginName === selectedPlugin.pluginName)
439
- ?.contributions ?? [])
440
- : []
442
+ installErrors={catalog.installErrors}
443
+ onQuery={catalog.setQuery}
444
+ onCompositionStart={catalog.beginComposition}
445
+ onCompositionEnd={catalog.endComposition}
446
+ onKind={catalog.setKind}
447
+ onSearchSource={(source) => {
448
+ catalog.setSearchSource(source)
449
+ if (
450
+ source === 'custom' &&
451
+ !catalog.registry &&
452
+ download.settings?.downloadSource.mode === 'custom'
453
+ ) {
454
+ catalog.setRegistry(download.settings.downloadSource.registry)
441
455
  }
442
- onRetry={() => void management.loadDetail(selectedPlugin.source)}
443
- />
444
- </div>
445
- ) : catalog.selection ? (
446
- catalog.detailLoading ? (
447
- <p role="status" className="py-4 text-sm text-muted-foreground">
448
- {t('loadingPackage')}
449
- </p>
450
- ) : catalog.detailError ? (
451
- <div role="alert" className="space-y-2 text-sm">
452
- <p className="text-destructive [overflow-wrap:anywhere]">{catalog.detailError}</p>
453
- <Button
454
- size="sm"
455
- variant="outline"
456
- onClick={() => void catalog.loadDetail(catalog.selection!)}
457
- >
458
- {t('retry')}
459
- </Button>
456
+ }}
457
+ onRegistry={catalog.setRegistry}
458
+ onDetail={openCatalog}
459
+ onInstall={installCatalogItem}
460
+ onUpdate={(source) => void management.execute({ kind: 'update', source })}
461
+ onMore={catalog.loadMore}
462
+ onRetry={catalog.retry}
463
+ onDirectInstall={openDirect}
464
+ />
465
+ </TabsContent>
466
+ </Tabs>
467
+ {detailOpen ? (
468
+ <div className="pi-desk-chat-scrollbar min-h-0 flex-1 overflow-y-auto p-4 sm:p-6">
469
+ {selectedPlugin ? (
470
+ <div className="min-w-0 space-y-5">
471
+ <div className="space-y-3">
472
+ <div className="flex flex-wrap items-center justify-between gap-3">
473
+ <div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
474
+ {selectedPlugin.version ? <span>v{selectedPlugin.version}</span> : null}
475
+ {selectedPlugin.updateAvailable ? (
476
+ <span className="rounded-md bg-status-success/10 px-2 py-0.5 text-xs font-medium text-status-success">
477
+ {selectedPlugin.availableVersion
478
+ ? t('newVersion', { version: selectedPlugin.availableVersion })
479
+ : t('updateAvailable')}
480
+ </span>
481
+ ) : null}
482
+ </div>
483
+ <L2PluginManagementControls
484
+ plugin={selectedPlugin}
485
+ displayName={pluginDisplayName(selectedPlugin)}
486
+ browserError={
487
+ selectedPlugin.pluginName
488
+ ? (browserErrors.get(selectedPlugin.pluginName) ?? null)
489
+ : null
490
+ }
491
+ disabled={disabled}
492
+ pending={busy(selectedPlugin.source)}
493
+ browserRetryPending={management.requests.has(
494
+ `entry:${selectedPlugin.pluginName}`
495
+ )}
496
+ onAction={(action) => management.handleControlAction(selectedPlugin, action)}
497
+ />
498
+ </div>
499
+ {selectedPlugin.description ? (
500
+ <p className="text-sm text-muted-foreground">
501
+ {selectL4LocalizedText(selectedPlugin.description, locale)}
502
+ </p>
503
+ ) : null}
504
+ <L2PluginManagementFeedback
505
+ plugin={selectedPlugin}
506
+ requestError={management.errors[pluginRequestKey(selectedPlugin.source)]}
507
+ browserError={
508
+ selectedPlugin.pluginName
509
+ ? (browserErrors.get(selectedPlugin.pluginName) ?? null)
510
+ : null
511
+ }
512
+ />
513
+ </div>
514
+ <Collapsible className="rounded-lg border">
515
+ <CollapsibleTrigger className="group flex min-h-9 w-full items-center justify-between px-3 text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring">
516
+ {t('advancedOptions')}
517
+ <ChevronDownIcon className="size-4 transition-transform group-aria-expanded:rotate-180" />
518
+ </CollapsibleTrigger>
519
+ <CollapsibleContent keepMounted className="hidden data-open:block">
520
+ <div className="flex flex-wrap items-center gap-3 px-3 pb-3">
521
+ {selectedPlugin.source.startsWith('npm:') ? (
522
+ <L2PluginManagementChannel
523
+ value={selectedPlugin.updateTag ?? 'latest'}
524
+ label={t('installChannel')}
525
+ disabled={disabled || busy(selectedPlugin.source)}
526
+ onChange={(tag) =>
527
+ void management.executeBatch({
528
+ action: 'tag',
529
+ sources: [selectedPlugin.source],
530
+ tag
531
+ })
532
+ }
533
+ />
534
+ ) : null}
535
+ <Button
536
+ size="sm"
537
+ variant="outline"
538
+ disabled={
539
+ disabled ||
540
+ (busy(selectedPlugin.source) &&
541
+ selectedPlugin.operation?.phase !== 'waiting')
542
+ }
543
+ onClick={() => management.handleControlAction(selectedPlugin, 'reload')}
544
+ >
545
+ {t('reloadOne')}
546
+ </Button>
547
+ </div>
548
+ </CollapsibleContent>
549
+ </Collapsible>
550
+ {management.detailLoading ? (
551
+ <p role="status" className="text-sm text-muted-foreground">
552
+ {t('loadingPackage')}
553
+ </p>
554
+ ) : null}
555
+ {management.detail?.readme ? (
556
+ <L2PluginReadme key={selectedPlugin.source} readme={management.detail.readme} />
557
+ ) : null}
558
+ <Collapsible className="border-t pt-2">
559
+ <CollapsibleTrigger className="group flex min-h-9 w-full items-center justify-between text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring">
560
+ {t('capabilityDetails')}
561
+ <ChevronDownIcon className="size-4 transition-transform group-aria-expanded:rotate-180" />
562
+ </CollapsibleTrigger>
563
+ <CollapsibleContent keepMounted className="hidden data-open:block">
564
+ <L2PluginCapabilityDetails
565
+ plugin={selectedPlugin}
566
+ detail={management.detail}
567
+ detailLoading={management.detailLoading}
568
+ detailError={management.detailError}
569
+ browserContributions={
570
+ selectedPlugin.pluginName
571
+ ? (management.pluginHost
572
+ .getRegisteredBrowserDescriptors()
573
+ .find(
574
+ (descriptor) => descriptor.pluginName === selectedPlugin.pluginName
575
+ )?.contributions ?? [])
576
+ : []
577
+ }
578
+ onRetry={() => void management.loadDetail(selectedPlugin.source)}
579
+ />
580
+ </CollapsibleContent>
581
+ </Collapsible>
582
+ {management.detailError ? (
583
+ <div role="alert" className="flex items-center gap-2 text-sm text-destructive">
584
+ <span>{management.detailError}</span>
585
+ <Button
586
+ size="sm"
587
+ variant="outline"
588
+ onClick={() => void management.loadDetail(selectedPlugin.source)}
589
+ >
590
+ {t('retry')}
591
+ </Button>
592
+ </div>
593
+ ) : null}
460
594
  </div>
461
- ) : catalog.detail ? (
462
- <div className="min-w-0 space-y-4">
463
- <div className="flex flex-wrap items-center gap-2 text-sm">
464
- <span>{t('installChannel')}</span>
465
- <L2PluginManagementChannel
466
- value={catalogTag}
467
- label={t('installChannel')}
468
- placeholder={t('defaultChannel')}
469
- disabled={catalogPending}
470
- onChange={(tag) => {
595
+ ) : catalog.selection ? (
596
+ <>
597
+ {catalog.detailLoading ? (
598
+ <p role="status" className="mb-3 text-sm text-muted-foreground">
599
+ {t('loadingPackage')}
600
+ </p>
601
+ ) : null}
602
+ {catalog.detailError ? (
603
+ <div role="alert" className="mb-3 space-y-2 text-sm">
604
+ <p className="text-destructive [overflow-wrap:anywhere]">{catalog.detailError}</p>
605
+ <Button
606
+ size="sm"
607
+ variant="outline"
608
+ onClick={() => void catalog.loadDetail(catalog.selection!)}
609
+ >
610
+ {t('retry')}
611
+ </Button>
612
+ </div>
613
+ ) : null}
614
+ {catalog.detail ? (
615
+ <L2PluginCatalogDetailView
616
+ detail={catalog.detail}
617
+ disabled={disabled || Boolean(catalog.detailError)}
618
+ pending={catalogPending || catalog.detailLoading}
619
+ installed={Boolean(
620
+ catalogPlugin &&
621
+ !(
622
+ catalogPlugin.operation?.action === 'add' &&
623
+ catalogPlugin.operation.phase === 'failed'
624
+ )
625
+ )}
626
+ installedVersion={catalogPlugin?.version ?? null}
627
+ updateAvailable={Boolean(catalogPlugin?.updateAvailable)}
628
+ error={
629
+ catalog.installErrors[catalog.detail.name] ??
630
+ management.errors[catalogKey] ??
631
+ (catalogPlugin?.operation?.phase === 'failed'
632
+ ? catalogPlugin.operation.message
633
+ : null)
634
+ }
635
+ operationMessage={
636
+ catalog.detailLoading
637
+ ? t('loadingPackage')
638
+ : operationMessage(catalogPlugin?.operation?.phase)
639
+ }
640
+ channel={catalogTag}
641
+ onChannel={(tag) => {
471
642
  setCatalogTag(tag)
472
643
  setCatalogVersionExplicit(false)
473
644
  void catalog.loadDetail(
@@ -475,60 +646,43 @@ export function L2PluginManagement({
475
646
  tag || catalogPlugin?.updateTag || 'latest'
476
647
  )
477
648
  }}
478
- />
479
- </div>
480
- <L2PluginCatalogDetailView
481
- detail={catalog.detail}
482
- disabled={disabled}
483
- pending={catalogPending}
484
- installed={Boolean(
485
- catalogPlugin &&
486
- !(
487
- catalogPlugin.operation?.action === 'add' &&
488
- catalogPlugin.operation.phase === 'failed'
489
- )
490
- )}
491
- installedVersion={catalogPlugin?.version ?? null}
492
- updateAvailable={Boolean(catalogPlugin?.updateAvailable)}
493
- error={
494
- catalog.installErrors[catalog.detail.name] ??
495
- management.errors[catalogKey] ??
496
- (catalogPlugin?.operation?.phase === 'failed'
497
- ? catalogPlugin.operation.message
498
- : null)
499
- }
500
- operationMessage={operationMessage(catalogPlugin?.operation?.phase)}
501
- downloadMode={catalog.downloadMode}
502
- downloadRegistry={catalog.downloadRegistry}
503
- onDownloadMode={catalog.setDownloadMode}
504
- onDownloadRegistry={catalog.setDownloadRegistry}
505
- onVersion={(version) => {
506
- setCatalogVersionExplicit(true)
507
- void catalog.loadDetail(catalog.selection!, version)
508
- }}
509
- onInstall={() =>
510
- catalog.installDetail((input, detail) =>
511
- management.requestInstall(
512
- {
513
- ...input,
514
- source: catalogVersionExplicit ? input.source : `npm:${detail.name}`,
515
- ...(catalogTag ? { tag: catalogTag } : {})
516
- },
517
- detail
649
+ downloadMode={catalog.downloadMode}
650
+ downloadRegistry={catalog.downloadRegistry}
651
+ onDownloadMode={catalog.setDownloadMode}
652
+ onDownloadRegistry={catalog.setDownloadRegistry}
653
+ onVersion={(version) => {
654
+ setCatalogVersionExplicit(true)
655
+ void catalog.loadDetail(catalog.selection!, version)
656
+ }}
657
+ onInstall={() =>
658
+ catalog.installDetail((input, detail) =>
659
+ management.requestInstall(
660
+ {
661
+ ...input,
662
+ source: catalogVersionExplicit ? input.source : `npm:${detail.name}`,
663
+ ...(catalogTag ? { tag: catalogTag } : {})
664
+ },
665
+ detail
666
+ )
518
667
  )
519
- )
520
- }
521
- onUpdate={() => {
522
- if (catalogPlugin)
523
- void management.execute({ kind: 'update', source: catalogPlugin.source })
524
- }}
525
- />
526
- </div>
527
- ) : null
528
- ) : management.detailSource ? (
529
- <p className="text-sm text-muted-foreground">{t('pluginRemoved')}</p>
530
- ) : null}
531
- </L2PluginManagementDialog>
668
+ }
669
+ onUpdate={() => {
670
+ if (catalogPlugin)
671
+ void management.execute({ kind: 'update', source: catalogPlugin.source })
672
+ }}
673
+ />
674
+ ) : null}
675
+ </>
676
+ ) : (
677
+ <p className="text-sm text-muted-foreground">{t('pluginRemoved')}</p>
678
+ )}
679
+ </div>
680
+ ) : null}
681
+ <L2PluginManagementDialog
682
+ confirmation={management.confirmation}
683
+ onClose={() => management.setConfirmation(null)}
684
+ onConfirm={(action) => void management.execute(action)}
685
+ />
532
686
  </div>
533
687
  )
534
688
  }