@jetcrab/pi-desk 1.1.1 → 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 (119) 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/prerender-manifest.json +3 -3
  67. package/temp/build/pi-desk/release/.next/react-loadable-manifest.json +1 -1
  68. package/temp/build/pi-desk/release/.next/required-server-files.js +3 -3
  69. package/temp/build/pi-desk/release/.next/required-server-files.json +3 -3
  70. package/temp/build/pi-desk/release/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  71. package/temp/build/pi-desk/release/.next/server/app/_global-error.html +1 -1
  72. package/temp/build/pi-desk/release/.next/server/app/_global-error.rsc +3 -3
  73. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_full.segment.rsc +3 -3
  74. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +3 -3
  75. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  76. package/temp/build/pi-desk/release/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  77. package/temp/build/pi-desk/release/.next/server/app/api/health/route.js +1 -1
  78. package/temp/build/pi-desk/release/.next/server/app/login/page.js +1 -1
  79. package/temp/build/pi-desk/release/.next/server/app/login/page_client-reference-manifest.js +1 -1
  80. package/temp/build/pi-desk/release/.next/server/app/page.js +79 -79
  81. package/temp/build/pi-desk/release/.next/server/app/page_client-reference-manifest.js +1 -1
  82. package/temp/build/pi-desk/release/.next/server/chunks/25406.js +8 -77
  83. package/temp/build/pi-desk/release/.next/server/chunks/{81107.js → 35348.js} +2 -2
  84. package/temp/build/pi-desk/release/.next/server/chunks/35985.js +4 -4
  85. package/temp/build/pi-desk/release/.next/server/chunks/36361.js +1 -1
  86. package/temp/build/pi-desk/release/.next/server/chunks/47757.js +6 -75
  87. package/temp/build/pi-desk/release/.next/server/chunks/50520.js +4 -4
  88. package/temp/build/pi-desk/release/.next/server/chunks/58330.js +1 -1
  89. package/temp/build/pi-desk/release/.next/server/chunks/84231.js +2 -2
  90. package/temp/build/pi-desk/release/.next/server/chunks/89236.js +2 -2
  91. package/temp/build/pi-desk/release/.next/server/chunks/92386.js +5 -5
  92. package/temp/build/pi-desk/release/.next/server/chunks/94518.js +1 -1
  93. package/temp/build/pi-desk/release/.next/server/chunks/9874.js +1 -1
  94. package/temp/build/pi-desk/release/.next/server/middleware-build-manifest.js +1 -1
  95. package/temp/build/pi-desk/release/.next/server/middleware-react-loadable-manifest.js +1 -1
  96. package/temp/build/pi-desk/release/.next/server/pages/500.html +1 -1
  97. package/temp/build/pi-desk/release/.next/server/server-reference-manifest.json +1 -1
  98. package/temp/build/pi-desk/release/.next/static/chunks/14197-3d3ddd426ac06c9c.js +3 -0
  99. package/temp/build/pi-desk/release/.next/static/chunks/34903-e4205c0becabea76.js +318 -0
  100. package/temp/build/pi-desk/release/.next/static/chunks/{54206.7c0792096df8a5cf.js → 54206.82a63ab971911eda.js} +3 -3
  101. package/temp/build/pi-desk/release/.next/static/chunks/74132-876f256c7a6101af.js +13 -0
  102. package/temp/build/pi-desk/release/.next/static/chunks/78923-17a9c337b29dcecd.js +1 -0
  103. package/temp/build/pi-desk/release/.next/static/chunks/96995-8f8313ea2ac3abd7.js +62 -0
  104. package/temp/build/pi-desk/release/.next/static/chunks/app/{layout-7c5409cced9c2501.js → layout-4faf3a52d219296d.js} +2 -2
  105. package/temp/build/pi-desk/release/.next/static/chunks/app/login/{page-8c87fb9e6c918848.js → page-bf893718bb151f3c.js} +1 -1
  106. package/temp/build/pi-desk/release/.next/static/chunks/app/page-28e1d77d6b884657.js +158 -0
  107. package/temp/build/pi-desk/release/.next/static/chunks/{webpack-45424d28099bbefa.js → webpack-7bfbf627379fb4c1.js} +1 -1
  108. package/temp/build/pi-desk/release/.next/static/css/3213ee28336bddc2.css +1 -0
  109. package/temp/build/pi-desk/release/.next/trace-build +1 -1
  110. package/src/server/l4_foundation/pidesk/l4-pidesk-skill-template.ts +0 -87
  111. package/temp/build/pi-desk/release/.next/static/chunks/27491-8b2d3e345b9142ea.js +0 -318
  112. package/temp/build/pi-desk/release/.next/static/chunks/7089-600a8f5593b7846c.js +0 -3
  113. package/temp/build/pi-desk/release/.next/static/chunks/74132-0884df6a6f11861a.js +0 -13
  114. package/temp/build/pi-desk/release/.next/static/chunks/78923-539a8b5f7af3c54a.js +0 -1
  115. package/temp/build/pi-desk/release/.next/static/chunks/96995-ae32167974365050.js +0 -62
  116. package/temp/build/pi-desk/release/.next/static/chunks/app/page-39b146293b6eb488.js +0 -158
  117. package/temp/build/pi-desk/release/.next/static/css/56486914a8ba0a1b.css +0 -1
  118. /package/temp/build/pi-desk/release/.next/static/{STVrBHA3z9KPtvBE4WcE2 → IgHJ40fniC07-ioYUKobL}/_buildManifest.js +0 -0
  119. /package/temp/build/pi-desk/release/.next/static/{STVrBHA3z9KPtvBE4WcE2 → IgHJ40fniC07-ioYUKobL}/_ssgManifest.js +0 -0
@@ -1,8 +1,7 @@
1
1
  'use client'
2
2
 
3
- import { Checkbox } from '@base-ui/react/checkbox'
4
- import { CheckIcon, MinusIcon } from 'lucide-react'
5
3
  import { useTranslation } from 'react-i18next'
4
+ import { MinusIcon } from 'lucide-react'
6
5
  import type {
7
6
  L2PluginManagementItem,
8
7
  L2PluginManagementSnapshot
@@ -10,192 +9,198 @@ import type {
10
9
  import { selectL4LocalizedText } from '@common/l4_foundation/locale/l4-localized-text'
11
10
  import { useL4Region } from '@client/l4_foundation/locale/l4-region-provider'
12
11
  import { Button } from '@client/l4_foundation/ui/shadcn/button'
12
+ import { Checkbox } from '@client/l4_foundation/ui/shadcn/checkbox'
13
13
  import { Input } from '@client/l4_foundation/ui/shadcn/input'
14
+ import {
15
+ Select,
16
+ SelectContent,
17
+ SelectItem,
18
+ SelectTrigger,
19
+ SelectValue
20
+ } from '@client/l4_foundation/ui/shadcn/select'
14
21
  import {
15
22
  L2PluginManagementControls,
23
+ L2PluginManagementFeedback,
16
24
  type L2PluginManagementControlAction
17
25
  } from '../l2-plugin-management-controls'
18
- import { L2PluginManagementChannel } from './l2-plugin-management-channel'
19
26
  import { pluginDisplayName, pluginRequestKey } from '../l2-use-plugin-management'
20
27
 
28
+ export type L2PluginStatusFilter = 'all' | 'updates' | 'disabled' | 'failed'
29
+
21
30
  interface InstalledListProps {
22
31
  snapshot: L2PluginManagementSnapshot | null
23
32
  loading: boolean
24
33
  filter: string
34
+ statusFilter: L2PluginStatusFilter
35
+ batchMode: boolean
25
36
  disabled: boolean
26
37
  requests: ReadonlySet<string>
27
38
  errors: Readonly<Record<string, string>>
28
39
  browserErrors: ReadonlyMap<string, string>
29
- channel: string
30
40
  selected: ReadonlySet<string>
31
- onChannel: (channel: string) => void
41
+ onStatusFilter: (filter: L2PluginStatusFilter) => void
42
+ onBatchMode: (value: boolean) => void
32
43
  onSelect: (keys: string[], selected: boolean) => void
33
- onCheckSelected: () => void
44
+ onUpdateAll: () => void
34
45
  onUpdateSelected: () => void
35
46
  onRemoveSelected: () => void
36
- onTagSelected: (tag: string) => void
37
47
  onFilter: (filter: string) => void
38
48
  onDetail: (source: string) => void
39
49
  onAction: (plugin: L2PluginManagementItem, action: L2PluginManagementControlAction) => void
40
- onDirectInstall: () => void
41
50
  onRefresh: () => void
42
51
  }
43
52
 
44
- export function L2PluginInstalledList({
45
- snapshot,
46
- loading,
47
- filter,
48
- disabled,
49
- requests,
50
- errors,
51
- browserErrors,
52
- channel,
53
- selected,
54
- onChannel,
55
- onSelect,
56
- onCheckSelected,
57
- onUpdateSelected,
58
- onRemoveSelected,
59
- onTagSelected,
60
- onFilter,
61
- onDetail,
62
- onAction,
63
- onDirectInstall,
64
- onRefresh
65
- }: InstalledListProps): React.JSX.Element {
53
+ export function L2PluginInstalledList(props: InstalledListProps): React.JSX.Element {
66
54
  const { t } = useTranslation('pluginManagement')
67
55
  const { locale } = useL4Region()
68
- const plugins =
69
- snapshot?.plugins.filter(
70
- (plugin) =>
71
- (!channel ||
72
- (plugin.source.startsWith('npm:') && (plugin.updateTag ?? 'latest') === channel)) &&
73
- `${pluginDisplayName(plugin)} ${plugin.description ? selectL4LocalizedText(plugin.description, locale) : ''}`
74
- .toLocaleLowerCase()
75
- .includes(filter.toLocaleLowerCase())
76
- ) ?? []
56
+ const all = props.snapshot?.plugins ?? []
57
+ const busy = (plugin: L2PluginManagementItem): boolean =>
58
+ props.requests.has(pluginRequestKey(plugin.source)) ||
59
+ Boolean(plugin.operation && plugin.operation.phase !== 'failed')
60
+ const failed = (plugin: L2PluginManagementItem): boolean =>
61
+ plugin.status === 'failed' ||
62
+ plugin.operation?.phase === 'failed' ||
63
+ Boolean(plugin.updateError || (plugin.pluginName && props.browserErrors.has(plugin.pluginName)))
64
+ const plugins = all.filter((plugin) => {
65
+ if (props.statusFilter === 'updates' && !plugin.updateAvailable) return false
66
+ if (props.statusFilter === 'disabled' && plugin.status !== 'disabled') return false
67
+ if (props.statusFilter === 'failed' && !failed(plugin)) return false
68
+ return `${pluginDisplayName(plugin)} ${plugin.description ? selectL4LocalizedText(plugin.description, locale) : ''}`
69
+ .toLocaleLowerCase()
70
+ .includes(props.filter.trim().toLocaleLowerCase())
71
+ })
77
72
  const selectable = plugins.filter((plugin) => plugin.kind !== 'extension')
78
- const selectedPlugins =
79
- snapshot?.plugins.filter((plugin) => selected.has(pluginRequestKey(plugin.source))) ?? []
80
- const selectedNpm = selectedPlugins.filter((plugin) => plugin.source.startsWith('npm:'))
81
- const selectedReady = selectedPlugins.filter(
82
- (plugin) =>
83
- !requests.has(pluginRequestKey(plugin.source)) &&
84
- (!plugin.operation || plugin.operation.phase === 'failed')
73
+ const selected = all.filter((plugin) => props.selected.has(pluginRequestKey(plugin.source)))
74
+ const updates = all.filter(
75
+ (plugin) => plugin.updateAvailable === true && plugin.kind !== 'extension' && !busy(plugin)
85
76
  )
77
+ const selectedUpdates = updates.filter((plugin) =>
78
+ props.selected.has(pluginRequestKey(plugin.source))
79
+ )
80
+ const selectedReady = selected.filter((plugin) => !busy(plugin))
86
81
  const allSelected =
87
82
  selectable.length > 0 &&
88
- selectable.every((plugin) => selected.has(pluginRequestKey(plugin.source)))
89
- const someSelected = selectable.some((plugin) => selected.has(pluginRequestKey(plugin.source)))
90
- const selectedNpmReady = selectedReady.filter((plugin) => plugin.source.startsWith('npm:'))
91
- const tooMany = selectedPlugins.length > 32
83
+ selectable.every((plugin) => props.selected.has(pluginRequestKey(plugin.source)))
84
+ const someSelected = selectable.some((plugin) =>
85
+ props.selected.has(pluginRequestKey(plugin.source))
86
+ )
87
+ const filters = (['all', 'updates', 'disabled', 'failed'] as const).map((value) => ({
88
+ value,
89
+ label: t(`statusFilter_${value}`)
90
+ }))
92
91
  return (
93
- <section aria-label={t('installed')} className="min-w-0 space-y-4">
94
- <div className="flex flex-wrap items-center gap-2">
95
- <label htmlFor="plugin-installed-filter" className="sr-only">
96
- {t('filterInstalled')}
97
- </label>
98
- <Input
99
- id="plugin-installed-filter"
100
- value={filter}
101
- onChange={(event) => onFilter(event.target.value)}
102
- placeholder={t('filterInstalled')}
103
- className="min-w-0 flex-1 basis-48"
104
- />
105
- <L2PluginManagementChannel
106
- value={channel}
107
- label={t('filterChannel')}
108
- placeholder={t('allChannels')}
109
- onChange={onChannel}
110
- />
111
- <Button size="sm" variant="outline" disabled={loading} onClick={onRefresh}>
112
- {t(loading ? 'checkingUpdates' : 'checkUpdates')}
113
- </Button>
114
- <Button size="sm" disabled={disabled} onClick={onDirectInstall}>
115
- {t('directInstall')}
116
- </Button>
117
- </div>
118
- {selectable.length || selectedPlugins.length ? (
119
- <div className="flex flex-wrap items-center gap-2 rounded-lg bg-muted p-2">
120
- <label className="flex min-h-8 cursor-pointer items-center gap-2 px-1 text-sm">
121
- <Checkbox.Root
122
- checked={allSelected}
123
- indeterminate={someSelected && !allSelected}
124
- disabled={!selectable.length}
125
- onCheckedChange={(checked) =>
126
- onSelect(
127
- selectable.map((plugin) => pluginRequestKey(plugin.source)),
128
- checked
129
- )
130
- }
131
- className="flex size-5 shrink-0 items-center justify-center rounded border border-input bg-background outline-none data-checked:bg-primary data-checked:text-primary-foreground focus-visible:ring-2 focus-visible:ring-ring"
92
+ <section aria-label={t('installed')} className="min-w-0 space-y-3">
93
+ <div className="sticky top-0 z-10 space-y-3 bg-popover pb-2">
94
+ <div className="flex flex-wrap items-center gap-2">
95
+ <Input
96
+ aria-label={t('filterInstalled')}
97
+ value={props.filter}
98
+ onChange={(event) => props.onFilter(event.target.value)}
99
+ placeholder={t('filterInstalled')}
100
+ className="min-w-40 flex-1 basis-52"
101
+ />
102
+ <Select
103
+ value={props.statusFilter}
104
+ items={filters}
105
+ onValueChange={(value) => {
106
+ if (value) props.onStatusFilter(value)
107
+ }}
108
+ >
109
+ <SelectTrigger aria-label={t('filterStatus')}>
110
+ <SelectValue />
111
+ </SelectTrigger>
112
+ <SelectContent positionerClassName="z-[160]">
113
+ {filters.map((item) => (
114
+ <SelectItem key={item.value} value={item.value}>
115
+ {item.label}
116
+ </SelectItem>
117
+ ))}
118
+ </SelectContent>
119
+ </Select>
120
+ <Button
121
+ size="sm"
122
+ variant="outline"
123
+ disabled={props.loading || props.disabled}
124
+ onClick={props.onRefresh}
125
+ >
126
+ {t(props.loading ? 'checkingUpdates' : 'checkUpdates')}
127
+ </Button>
128
+ {updates.length > 0 ? (
129
+ <Button
130
+ size="sm"
131
+ disabled={props.disabled || props.loading}
132
+ onClick={props.onUpdateAll}
132
133
  >
133
- <Checkbox.Indicator>
134
- {someSelected && !allSelected ? (
135
- <MinusIcon className="size-4" />
136
- ) : (
137
- <CheckIcon className="size-4" />
138
- )}
139
- </Checkbox.Indicator>
140
- </Checkbox.Root>
141
- {t('selectFiltered')}
142
- </label>
143
- {selectedPlugins.length ? (
144
- <>
145
- <span className="text-sm text-muted-foreground">
146
- {t('selectedCount', { count: selectedPlugins.length })}
147
- </span>
148
- <Button
149
- size="sm"
150
- variant="outline"
151
- disabled={loading || !selectedNpm.length || tooMany}
152
- onClick={onCheckSelected}
153
- >
154
- {t('checkSelected')}
155
- </Button>
156
- <Button
157
- size="sm"
158
- variant="outline"
159
- disabled={disabled || !selectedReady.length || tooMany}
160
- onClick={onUpdateSelected}
161
- >
162
- {t('updateSelected')}
163
- </Button>
164
- <Button
165
- size="sm"
166
- variant="outline"
167
- disabled={disabled || !selectedReady.length || tooMany}
168
- onClick={onRemoveSelected}
169
- >
170
- {t('removeSelected')}
171
- </Button>
172
- <L2PluginManagementChannel
173
- value=""
174
- label={t('setChannel')}
175
- placeholder={t('setChannel')}
176
- disabled={disabled || !selectedNpmReady.length || tooMany}
177
- onChange={(tag) => {
178
- if (tag) onTagSelected(tag)
179
- }}
180
- />
181
- <Button size="sm" variant="ghost" onClick={() => onSelect([...selected], false)}>
182
- {t('clearSelection')}
183
- </Button>
184
- </>
134
+ {t('updateAll', { count: updates.length })}
135
+ </Button>
185
136
  ) : null}
186
- {tooMany ? (
187
- <p role="status" className="w-full text-sm text-muted-foreground">
188
- {t('batchLimit')}
189
- </p>
137
+ {all.some((plugin) => plugin.kind !== 'extension') ? (
138
+ <Button size="sm" variant="ghost" onClick={() => props.onBatchMode(!props.batchMode)}>
139
+ {t(props.batchMode ? 'finishSelection' : 'batchManage')}
140
+ </Button>
190
141
  ) : null}
191
142
  </div>
192
- ) : null}
193
- {errors.list ? (
194
- <p role="alert" className="text-sm text-destructive [overflow-wrap:anywhere]">
195
- {errors.list}
196
- </p>
197
- ) : null}
198
- {loading && !snapshot ? (
143
+ {props.batchMode ? (
144
+ <div className="flex flex-wrap items-center gap-3 rounded-lg bg-muted px-3 py-2">
145
+ <label className="flex min-h-8 cursor-pointer items-center gap-2 text-sm">
146
+ <span className="relative flex size-4 items-center justify-center">
147
+ <Checkbox
148
+ checked={allSelected}
149
+ indeterminate={someSelected && !allSelected}
150
+ className="data-indeterminate:[&_svg]:hidden"
151
+ disabled={!selectable.length}
152
+ onCheckedChange={(checked) =>
153
+ props.onSelect(
154
+ selectable.map((plugin) => pluginRequestKey(plugin.source)),
155
+ checked
156
+ )
157
+ }
158
+ />
159
+ {someSelected && !allSelected ? (
160
+ <MinusIcon aria-hidden className="pointer-events-none absolute size-3.5" />
161
+ ) : null}
162
+ </span>
163
+ {t(props.filter || props.statusFilter !== 'all' ? 'selectFiltered' : 'selectAll')}
164
+ </label>
165
+ <span className="text-sm text-muted-foreground">
166
+ {t('selectedCount', { count: selected.length })}
167
+ </span>
168
+ {selected.length ? (
169
+ <>
170
+ <Button
171
+ size="sm"
172
+ variant="outline"
173
+ disabled={props.disabled || props.loading || !selectedUpdates.length}
174
+ onClick={props.onUpdateSelected}
175
+ >
176
+ {t('updateSelected')}
177
+ </Button>
178
+ <Button
179
+ size="sm"
180
+ variant="outline"
181
+ disabled={props.disabled || !selectedReady.length}
182
+ onClick={props.onRemoveSelected}
183
+ >
184
+ {t('removeSelected')}
185
+ </Button>
186
+ <Button
187
+ size="sm"
188
+ variant="ghost"
189
+ onClick={() => props.onSelect([...props.selected], false)}
190
+ >
191
+ {t('clearSelection')}
192
+ </Button>
193
+ </>
194
+ ) : null}
195
+ </div>
196
+ ) : null}
197
+ {props.errors.list ? (
198
+ <p role="alert" className="text-sm text-destructive [overflow-wrap:anywhere]">
199
+ {props.errors.list}
200
+ </p>
201
+ ) : null}
202
+ </div>
203
+ {props.loading && !props.snapshot ? (
199
204
  <p role="status" className="py-6 text-sm text-muted-foreground">
200
205
  {t('loading')}
201
206
  </p>
@@ -204,76 +209,81 @@ export function L2PluginInstalledList({
204
209
  {plugins.map((plugin) => {
205
210
  const name = pluginDisplayName(plugin)
206
211
  const key = pluginRequestKey(plugin.source)
212
+ const browserError = plugin.pluginName
213
+ ? (props.browserErrors.get(plugin.pluginName) ?? null)
214
+ : null
207
215
  return (
208
216
  <article
209
217
  key={key}
210
- className={`min-w-0 space-y-3 p-4 ${selected.has(key) ? 'bg-accent/40' : ''}`}
218
+ aria-label={name}
219
+ className={`min-w-0 px-4 py-3 ${props.selected.has(key) && props.batchMode ? 'bg-accent/40' : ''}`}
211
220
  >
212
- <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
213
- {plugin.kind !== 'extension' ? (
214
- <Checkbox.Root
215
- checked={selected.has(key)}
216
- aria-label={t('selectPlugin', { name })}
217
- onCheckedChange={(checked) => onSelect([key], checked)}
218
- className="flex size-6 shrink-0 items-center justify-center rounded border border-input bg-background outline-none data-checked:bg-primary data-checked:text-primary-foreground focus-visible:ring-2 focus-visible:ring-ring"
219
- >
220
- <Checkbox.Indicator>
221
- <CheckIcon className="size-4" />
222
- </Checkbox.Indicator>
223
- </Checkbox.Root>
224
- ) : null}
225
- <Button
226
- variant="link"
227
- className="h-auto min-w-0 max-w-full justify-start p-0 text-left text-sm font-semibold whitespace-normal [overflow-wrap:anywhere]"
228
- aria-label={t('viewDetails', { name })}
229
- onClick={() => onDetail(plugin.source)}
230
- >
231
- {name}
232
- </Button>
233
- {plugin.version ? (
234
- <span className="text-xs text-muted-foreground">v{plugin.version}</span>
221
+ <div className="flex items-start gap-3">
222
+ {props.batchMode && plugin.kind !== 'extension' ? (
223
+ <div className="flex min-h-8 shrink-0 items-center">
224
+ <Checkbox
225
+ checked={props.selected.has(key)}
226
+ aria-label={t('selectPlugin', { name })}
227
+ onCheckedChange={(checked) => props.onSelect([key], checked)}
228
+ />
229
+ </div>
235
230
  ) : null}
236
- </div>
237
- {plugin.source.startsWith('npm:') ? (
238
- <div className="flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground [overflow-wrap:anywhere]">
239
- <span>{t('followingChannel', { tag: plugin.updateTag ?? 'latest' })}</span>
240
- {plugin.availableVersion ? (
241
- <span>{t('targetVersion', { version: plugin.availableVersion })}</span>
242
- ) : null}
243
- {plugin.updateError ? (
244
- <p role="alert" className="w-full text-sm text-destructive">
245
- {t('itemCheckFailed', { error: plugin.updateError })}
231
+ <div className="min-w-0 flex-1 space-y-1">
232
+ <div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
233
+ <div className="flex min-w-0 flex-1 basis-64 flex-wrap items-center gap-x-2 gap-y-1">
234
+ <Button
235
+ variant="link"
236
+ className="h-auto min-h-8 min-w-0 max-w-full justify-start p-0 text-left text-sm font-semibold whitespace-normal no-underline hover:underline [overflow-wrap:anywhere]"
237
+ aria-label={t('viewDetails', { name })}
238
+ onClick={() => props.onDetail(plugin.source)}
239
+ >
240
+ {name}
241
+ </Button>
242
+ {plugin.version ? (
243
+ <span className="text-xs text-muted-foreground">v{plugin.version}</span>
244
+ ) : null}
245
+ {plugin.updateAvailable ? (
246
+ <span className="inline-flex rounded-md bg-status-success/10 px-2 py-0.5 text-xs font-medium text-status-success">
247
+ {plugin.availableVersion
248
+ ? t('newVersion', { version: plugin.availableVersion })
249
+ : t('updateAvailable')}
250
+ </span>
251
+ ) : null}
252
+ {plugin.updateTag && plugin.updateTag !== 'latest' ? (
253
+ <span className="text-xs text-muted-foreground">
254
+ {plugin.updateTag === 'dev' ? t('channelDev') : plugin.updateTag}
255
+ </span>
256
+ ) : null}
257
+ </div>
258
+ <L2PluginManagementControls
259
+ plugin={plugin}
260
+ displayName={name}
261
+ browserError={browserError}
262
+ disabled={props.disabled}
263
+ pending={busy(plugin)}
264
+ browserRetryPending={props.requests.has(`entry:${plugin.pluginName}`)}
265
+ onAction={(action) => props.onAction(plugin, action)}
266
+ />
267
+ </div>
268
+ {plugin.description ? (
269
+ <p className="line-clamp-2 text-sm text-muted-foreground [overflow-wrap:anywhere]">
270
+ {selectL4LocalizedText(plugin.description, locale)}
246
271
  </p>
247
272
  ) : null}
273
+ <L2PluginManagementFeedback
274
+ plugin={plugin}
275
+ browserError={browserError}
276
+ requestError={props.errors[key] ?? props.errors[`entry:${plugin.pluginName}`]}
277
+ />
248
278
  </div>
249
- ) : null}
250
- <p className="text-sm text-muted-foreground [overflow-wrap:anywhere]">
251
- {plugin.description
252
- ? selectL4LocalizedText(plugin.description, locale)
253
- : t('noPluginDescription')}
254
- </p>
255
- <L2PluginManagementControls
256
- plugin={plugin}
257
- displayName={name}
258
- browserError={
259
- plugin.pluginName ? (browserErrors.get(plugin.pluginName) ?? null) : null
260
- }
261
- requestError={errors[key] ?? errors[`entry:${plugin.pluginName}`]}
262
- disabled={disabled}
263
- pending={
264
- requests.has(key) ||
265
- Boolean(plugin.operation && plugin.operation.phase !== 'failed')
266
- }
267
- browserRetryPending={requests.has(`entry:${plugin.pluginName}`)}
268
- onAction={(action) => onAction(plugin, action)}
269
- />
279
+ </div>
270
280
  </article>
271
281
  )
272
282
  })}
273
283
  </div>
274
284
  ) : (
275
285
  <p className="py-8 text-center text-sm text-muted-foreground">
276
- {t(filter || channel ? 'noMatches' : 'empty')}
286
+ {t(props.filter || props.statusFilter !== 'all' ? 'noMatches' : 'empty')}
277
287
  </p>
278
288
  )}
279
289
  </section>
@@ -28,23 +28,30 @@ export function L2PluginManagementChannel({
28
28
  const options = [
29
29
  ...(placeholder ? [{ value: '', label: placeholder }] : []),
30
30
  { value: 'latest', label: t('channelLatest') },
31
- { value: 'dev', label: t('channelDev') }
31
+ { value: 'dev', label: t('channelDev') },
32
+ ...(value && !['latest', 'dev'].includes(value) ? [{ value, label: value }] : [])
32
33
  ]
33
34
  return (
34
35
  <Select
35
36
  value={value}
36
37
  items={options}
37
- disabled={disabled}
38
+ onOpenChange={(open, details) => {
39
+ if (open && disabled) details.cancel()
40
+ }}
38
41
  onValueChange={(next) => {
39
- if (next !== null) onChange(next)
42
+ if (next !== null && !disabled) onChange(next)
40
43
  }}
41
44
  >
42
- <SelectTrigger aria-label={label} className="max-w-full">
45
+ <SelectTrigger
46
+ aria-label={label}
47
+ aria-disabled={disabled}
48
+ className="max-w-full aria-disabled:opacity-50"
49
+ >
43
50
  <SelectValue />
44
51
  </SelectTrigger>
45
52
  <SelectContent positionerClassName="z-[160]">
46
53
  {options.map((option) => (
47
- <SelectItem key={option.value} value={option.value}>
54
+ <SelectItem key={option.value} value={option.value} disabled={disabled}>
48
55
  {option.label}
49
56
  </SelectItem>
50
57
  ))}