@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,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { PlusIcon } from 'lucide-react'
3
+ import { LoaderCircleIcon, PlusIcon } from 'lucide-react'
4
4
  import { useId, useMemo } from 'react'
5
5
  import type { BrowserBeforeLeaveHandler } from '@jetcrab/pi-desk-sdk/browser'
6
6
  import { useL4AppSocket } from '@client/l4_foundation/realtime/app-socket/l4-app-socket'
@@ -43,14 +43,12 @@ export function L2PiSettings({
43
43
  )
44
44
  }, [projects, focusedCwd])
45
45
  const scopeLabel =
46
- state.cwd === null
46
+ state.displayCwd === null
47
47
  ? t('全局')
48
- : `${scopeOptions.find((project) => project.cwd === state.cwd)?.projectName ?? t('当前项目')} · ${state.cwd}`
48
+ : `${scopeOptions.find((project) => project.cwd === state.displayCwd)?.projectName ?? t('当前项目')} · ${state.displayCwd}`
49
49
  const editing = Boolean(state.editor && state.mcp)
50
- const empty = Boolean(
51
- state.mcp && !state.loading && !state.loadError && !editing && state.rows.length === 0
52
- )
53
50
  const busy = state.pending || Boolean(state.check?.loading)
51
+ const disabled = busy || !state.ready
54
52
  const selectedName = state.editor?.kind === 'manual' ? state.editor.draft.originalName : null
55
53
  const addManually = (): void => {
56
54
  void state.navigate(() => state.startManual(null))
@@ -64,35 +62,30 @@ export function L2PiSettings({
64
62
  return (
65
63
  <section aria-label="MCP" className="@container/mcp flex h-full min-h-0 min-w-0 flex-col">
66
64
  {state.dialog}
67
- <header
68
- className={cn(
69
- 'grid shrink-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-3 border-b p-4',
70
- !empty && '@min-[40rem]/mcp:grid-cols-[auto_minmax(0,1fr)_auto]'
71
- )}
72
- >
73
- <h2 className="text-xl font-semibold">MCP</h2>
74
- {!empty && (
75
- <div className="col-start-2 flex flex-wrap justify-end gap-2 @min-[40rem]/mcp:col-start-3 @min-[40rem]/mcp:self-end">
76
- <Button disabled={busy || state.loading || !state.mcp} onClick={addManually}>
77
- <PlusIcon aria-hidden="true" />
78
- {t('添加服务')}
79
- </Button>
80
- <Button
81
- variant="outline"
82
- disabled={busy || state.loading || !state.mcp}
83
- onClick={importJson}
84
- >
85
- {t('导入 JSON')}
86
- </Button>
87
- </div>
88
- )}
65
+ <header className="grid shrink-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-3 border-b p-4 @min-[40rem]/mcp:grid-cols-[auto_minmax(0,1fr)_auto]">
66
+ <h2 className="flex items-center gap-2 text-xl font-semibold">
67
+ MCP
68
+ <span className="inline-flex size-4">
69
+ {state.loading && (
70
+ <LoaderCircleIcon
71
+ role="status"
72
+ aria-label={t('正在读取配置…')}
73
+ className="size-4 animate-spin text-muted-foreground"
74
+ />
75
+ )}
76
+ </span>
77
+ </h2>
78
+ <div className="col-start-2 flex flex-wrap justify-end gap-2 @min-[40rem]/mcp:col-start-3 @min-[40rem]/mcp:self-end">
79
+ <Button disabled={disabled} onClick={addManually}>
80
+ <PlusIcon aria-hidden="true" />
81
+ {t('添加服务')}
82
+ </Button>
83
+ <Button variant="outline" disabled={disabled} onClick={importJson}>
84
+ {t('导入 JSON')}
85
+ </Button>
86
+ </div>
89
87
  {/* Select 尾随隐藏输入,space-y 会给选择框追加底部间距;并排工具栏中需清除。 */}
90
- <div
91
- className={cn(
92
- 'col-span-2 row-start-2 w-full @min-[40rem]/mcp:col-span-1 @min-[40rem]/mcp:col-start-2 @min-[40rem]/mcp:row-start-1 @min-[40rem]/mcp:max-w-80 @min-[40rem]/mcp:justify-self-end @min-[40rem]/mcp:[&_[data-slot=select-trigger]]:mb-0',
93
- empty && '@min-[40rem]/mcp:w-56'
94
- )}
95
- >
88
+ <div className="col-span-2 row-start-2 w-full @min-[40rem]/mcp:col-span-1 @min-[40rem]/mcp:col-start-2 @min-[40rem]/mcp:row-start-1 @min-[40rem]/mcp:max-w-80 @min-[40rem]/mcp:justify-self-end @min-[40rem]/mcp:[&_[data-slot=select-trigger]]:mb-0">
96
89
  <L2PiSettingsSelect
97
90
  id={`${id}-scope`}
98
91
  label={t('配置范围')}
@@ -106,8 +99,7 @@ export function L2PiSettings({
106
99
  }))
107
100
  ]}
108
101
  onChange={(value) => {
109
- if (value !== (state.cwd ?? ''))
110
- void state.navigate(() => state.setCwd(value || null))
102
+ void state.setCwd(value || null)
111
103
  }}
112
104
  />
113
105
  </div>
@@ -132,19 +124,20 @@ export function L2PiSettings({
132
124
  ))}
133
125
  </div>
134
126
  )}
135
- {state.loadError ? (
127
+ {state.loadError && (
136
128
  <div className="space-y-3 p-4">
137
129
  <L2PiSettingsError message={state.loadError} />
138
130
  <Button variant="outline" onClick={state.retry} disabled={state.loading}>
139
131
  {t('重试')}
140
132
  </Button>
141
133
  </div>
142
- ) : state.loading && !editing ? (
134
+ )}
135
+ {state.loading && !state.mcp ? (
143
136
  <p role="status" className="p-4 text-sm text-muted-foreground">
144
137
  {t('正在读取配置…')}
145
138
  </p>
146
139
  ) : state.mcp ? (
147
- <div className="flex min-h-0 min-w-0 flex-1">
140
+ <div aria-busy={state.loading} className="flex min-h-0 min-w-0 flex-1">
148
141
  {state.editor?.kind !== 'json' && state.rows.length > 0 && (
149
142
  <div
150
143
  className={cn(
@@ -162,7 +155,7 @@ export function L2PiSettings({
162
155
  <L2McpSettingsList
163
156
  rows={state.rows}
164
157
  selectedName={selectedName}
165
- pending={busy}
158
+ pending={disabled}
166
159
  onSelect={(row) => {
167
160
  if (selectedName === row.name) return
168
161
  void state.navigate(() =>
@@ -179,11 +172,12 @@ export function L2PiSettings({
179
172
  key={state.editor.kind === 'manual' ? (selectedName ?? 'new') : 'json'}
180
173
  editor={state.editor}
181
174
  scopeLabel={scopeLabel}
182
- project={state.cwd !== null}
175
+ project={state.displayCwd !== null}
183
176
  projectTrusted={state.mcp.projectTrusted}
184
177
  local={state.mcp.local}
185
178
  inherited={state.mcp.inherited}
186
- pending={busy}
179
+ pending={disabled}
180
+ saving={state.pending}
187
181
  error={state.error}
188
182
  check={state.check?.name === selectedName ? state.check : null}
189
183
  onDraft={state.updateDraft}
@@ -200,7 +194,7 @@ export function L2PiSettings({
200
194
  onDismissCheck={state.dismissCheck}
201
195
  />
202
196
  ) : state.rows.length === 0 ? (
203
- <L2McpSettingsEmpty pending={busy} onAdd={addManually} onImport={importJson} />
197
+ <L2McpSettingsEmpty pending={disabled} onAdd={addManually} onImport={importJson} />
204
198
  ) : null}
205
199
  </div>
206
200
  ) : null}
@@ -27,6 +27,7 @@ export function L2McpSettingsEditor({
27
27
  local,
28
28
  inherited,
29
29
  pending,
30
+ saving,
30
31
  error,
31
32
  onDraft,
32
33
  onJson,
@@ -45,6 +46,7 @@ export function L2McpSettingsEditor({
45
46
  local: Record<string, L2McpServerConfig>
46
47
  inherited: Record<string, L2McpServerConfig>
47
48
  pending: boolean
49
+ saving: boolean
48
50
  error: { field: string; message: string } | null
49
51
  onDraft: (patch: Partial<L2McpDraft>) => void
50
52
  onJson: (text: string) => void
@@ -402,7 +404,7 @@ export function L2McpSettingsEditor({
402
404
  (!editor.preview || (collisions.length > 0 && !editor.overwrite)))
403
405
  }
404
406
  >
405
- {t(check?.loading ? '正在测试连接…' : pending ? '保存中…' : '保存')}
407
+ {t(check?.loading ? '正在测试连接…' : saving ? '保存中…' : '保存')}
406
408
  </Button>
407
409
  </footer>
408
410
  </form>
@@ -28,7 +28,7 @@ export function L2McpSettingsList({
28
28
  disabled={pending}
29
29
  onClick={() => onSelect(row)}
30
30
  className={cn(
31
- 'flex w-full min-w-0 cursor-pointer items-center gap-3 p-4 text-left outline-none transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring disabled:cursor-default disabled:opacity-50',
31
+ 'flex w-full min-w-0 cursor-pointer items-center gap-3 p-4 text-left outline-none transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring disabled:cursor-default',
32
32
  selectedName === row.name && 'bg-accent'
33
33
  )}
34
34
  >
@@ -185,7 +185,6 @@ export function useL2PluginCatalog(
185
185
  const epoch = ++detailEpoch.current
186
186
  setDetailLoading(true)
187
187
  setDetailError(null)
188
- setDetail(null)
189
188
  setInstallErrors((current) => {
190
189
  const next = { ...current }
191
190
  delete next[item.name]
@@ -203,6 +202,9 @@ export function useL2PluginCatalog(
203
202
  }
204
203
  const openDetail = (item: L2PluginCatalogItem): void => {
205
204
  setSelection(item)
205
+ setDetail(null)
206
+ setDownloadMode('default')
207
+ setDownloadRegistry('')
206
208
  void loadDetail(item)
207
209
  }
208
210
  const closeDetail = (): void => {
@@ -21,7 +21,10 @@ interface PluginDownloadSourceState {
21
21
  retry: () => Promise<void>
22
22
  }
23
23
 
24
- export function useL2PluginDownloadSource(biz: L2PluginManagementBiz): PluginDownloadSourceState {
24
+ export function useL2PluginDownloadSource(
25
+ biz: L2PluginManagementBiz,
26
+ onSaved: () => void
27
+ ): PluginDownloadSourceState {
25
28
  const { t } = useTranslation('pluginManagement')
26
29
  const [settings, setSettings] = useState<L2PluginCatalogSettings | null>(null)
27
30
  const [mode, setMode] = useState<L2PluginDownloadSource['mode']>('auto')
@@ -85,6 +88,7 @@ export function useL2PluginDownloadSource(biz: L2PluginManagementBiz): PluginDow
85
88
  if (mounted.current) {
86
89
  setSettings(next)
87
90
  setMode(next.downloadSource.mode)
91
+ onSaved()
88
92
  }
89
93
  } catch (cause) {
90
94
  failedRequest.current = 'save'
@@ -396,14 +396,20 @@ function ToolParameters({ tool }: { tool: L2PluginManagementToolDetail }): React
396
396
  ) : (
397
397
  <p className="text-xs text-muted-foreground">{t('noArguments')}</p>
398
398
  )}
399
- <details className="mt-3">
400
- <summary className="cursor-pointer text-sm text-muted-foreground">
399
+ <Collapsible className="mt-3">
400
+ <CollapsibleTrigger className="group flex min-h-8 cursor-pointer items-center gap-2 text-sm text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring">
401
+ <ChevronRightIcon
402
+ aria-hidden="true"
403
+ className="size-3.5 shrink-0 transition-transform group-aria-expanded:rotate-90"
404
+ />
401
405
  {t('rawSchema')}
402
- </summary>
403
- <pre className="mt-2 whitespace-pre-wrap break-words rounded-lg bg-muted p-3 font-mono text-sm leading-[1.6]">
404
- {JSON.stringify(tool.parameters, null, 2)}
405
- </pre>
406
- </details>
406
+ </CollapsibleTrigger>
407
+ <CollapsibleContent keepMounted>
408
+ <pre className="mt-2 whitespace-pre-wrap break-words rounded-lg bg-muted p-3 font-mono text-sm leading-[1.6]">
409
+ {JSON.stringify(tool.parameters, null, 2)}
410
+ </pre>
411
+ </CollapsibleContent>
412
+ </Collapsible>
407
413
  </div>
408
414
  </div>
409
415
  )
@@ -416,9 +422,13 @@ function ToolsSection({ detail }: { detail: L2PluginManagementDetail }): React.J
416
422
  <Section title={t('toolCapabilities')} description={t('toolDescription')}>
417
423
  <div className="space-y-2">
418
424
  {detail.tools.map((tool, index) => (
419
- <details key={`${tool.name}:${index}`} className="border-b py-3 last:border-b-0">
420
- <summary className="cursor-pointer text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring">
421
- <div className="inline-flex min-w-0 flex-wrap items-center gap-2">
425
+ <Collapsible key={`${tool.name}:${index}`} className="border-b py-3 last:border-b-0">
426
+ <CollapsibleTrigger className="group w-full cursor-pointer text-left text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring">
427
+ <ChevronRightIcon
428
+ aria-hidden="true"
429
+ className="mr-2 inline-block size-3.5 shrink-0 transition-transform group-aria-expanded:rotate-90"
430
+ />
431
+ <span className="inline-flex min-w-0 flex-wrap items-center gap-2">
422
432
  <code className="font-semibold">{tool.name}</code>
423
433
  {tool.label && tool.label !== tool.name ? (
424
434
  <span className="text-xs text-muted-foreground">{tool.label}</span>
@@ -429,34 +439,36 @@ function ToolsSection({ detail }: { detail: L2PluginManagementDetail }): React.J
429
439
  {t('overridesBuiltin')}
430
440
  </span>
431
441
  ) : null}
432
- </div>
433
- <p className="mt-1.5 break-words text-sm leading-5 text-muted-foreground">
442
+ </span>
443
+ <span className="mt-1.5 block break-words text-sm leading-5 text-muted-foreground">
434
444
  {tool.description || t('noDescription')}
435
- </p>
436
- </summary>
437
- <ToolParameters tool={tool} />
438
- {tool.promptSnippet || tool.promptGuidelines.length > 0 ? (
439
- <div className="mt-3 space-y-2">
440
- <div className="flex flex-wrap items-center gap-2">
441
- <p className="text-xs font-medium">{t('systemPrompts')}</p>
442
- {tool.state !== 'active' ? (
443
- <span className="text-sm text-muted-foreground">{t('inactivePrompt')}</span>
445
+ </span>
446
+ </CollapsibleTrigger>
447
+ <CollapsibleContent keepMounted>
448
+ <ToolParameters tool={tool} />
449
+ {tool.promptSnippet || tool.promptGuidelines.length > 0 ? (
450
+ <div className="mt-3 space-y-2">
451
+ <div className="flex flex-wrap items-center gap-2">
452
+ <p className="text-xs font-medium">{t('systemPrompts')}</p>
453
+ {tool.state !== 'active' ? (
454
+ <span className="text-sm text-muted-foreground">{t('inactivePrompt')}</span>
455
+ ) : null}
456
+ </div>
457
+ {tool.promptSnippet ? (
458
+ <PromptTextBlock title="Available tools" text={tool.promptSnippet} />
459
+ ) : null}
460
+ {tool.promptGuidelines.length > 0 ? (
461
+ <PromptTextBlock
462
+ title={`Guidelines · ${tool.promptGuidelines.length}`}
463
+ text={tool.promptGuidelines
464
+ .map((guideline, guidelineIndex) => `${guidelineIndex + 1}. ${guideline}`)
465
+ .join('\n\n')}
466
+ />
444
467
  ) : null}
445
468
  </div>
446
- {tool.promptSnippet ? (
447
- <PromptTextBlock title="Available tools" text={tool.promptSnippet} />
448
- ) : null}
449
- {tool.promptGuidelines.length > 0 ? (
450
- <PromptTextBlock
451
- title={`Guidelines · ${tool.promptGuidelines.length}`}
452
- text={tool.promptGuidelines
453
- .map((guideline, guidelineIndex) => `${guidelineIndex + 1}. ${guideline}`)
454
- .join('\n\n')}
455
- />
456
- ) : null}
457
- </div>
458
- ) : null}
459
- </details>
469
+ ) : null}
470
+ </CollapsibleContent>
471
+ </Collapsible>
460
472
  ))}
461
473
  </div>
462
474
  </Section>
@@ -474,11 +486,15 @@ function ResourceContent({
474
486
  }): React.JSX.Element {
475
487
  const { t } = useTranslation('pluginManagement')
476
488
  return (
477
- <details className="mt-3">
478
- <summary className="cursor-pointer py-2 text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring">
489
+ <Collapsible className="mt-3">
490
+ <CollapsibleTrigger className="group flex min-h-8 w-full cursor-pointer items-center gap-2 py-2 text-left text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring">
491
+ <ChevronRightIcon
492
+ aria-hidden="true"
493
+ className="size-3.5 shrink-0 transition-transform group-aria-expanded:rotate-90"
494
+ />
479
495
  {title}
480
- </summary>
481
- <div className="border-t">
496
+ </CollapsibleTrigger>
497
+ <CollapsibleContent keepMounted className="border-t">
482
498
  <div className="flex justify-end px-2 pt-1">
483
499
  <CopyTextButton text={content} label={t('copyNamed', { name: title })} />
484
500
  </div>
@@ -486,8 +502,8 @@ function ResourceContent({
486
502
  {content}
487
503
  </pre>
488
504
  {truncated ? <p className="py-2 text-sm text-status-warning">{t('truncated')}</p> : null}
489
- </div>
490
- </details>
505
+ </CollapsibleContent>
506
+ </Collapsible>
491
507
  )
492
508
  }
493
509
 
@@ -6,6 +6,7 @@ import type { L2PluginManagementItem } from '@common/l2_biz/plugin/l2-plugin-man
6
6
  import { selectL4LocalizedText } from '@common/l4_foundation/locale/l4-localized-text'
7
7
  import { useL4Region } from '@client/l4_foundation/locale/l4-region-provider'
8
8
  import { Button } from '@client/l4_foundation/ui/shadcn/button'
9
+ import { Switch } from '@client/l4_foundation/ui/shadcn/switch'
9
10
 
10
11
  export type L2PluginManagementControlAction =
11
12
  'reload' | 'update' | 'remove' | 'retry' | 'retry-browser' | 'enable' | 'disable'
@@ -14,7 +15,6 @@ interface L2PluginManagementControlsProps {
14
15
  plugin: L2PluginManagementItem
15
16
  displayName: string
16
17
  browserError: string | null
17
- requestError?: string | null
18
18
  disabled: boolean
19
19
  pending: boolean
20
20
  browserRetryPending: boolean
@@ -25,142 +25,115 @@ export function L2PluginManagementControls({
25
25
  plugin,
26
26
  displayName,
27
27
  browserError,
28
- requestError,
29
28
  disabled,
30
29
  pending,
31
30
  browserRetryPending,
32
31
  onAction
33
32
  }: L2PluginManagementControlsProps): React.JSX.Element {
34
33
  const { t } = useTranslation('pluginManagement')
35
- const { locale } = useL4Region()
36
34
  const phase = plugin.operation?.phase
37
- const unfinished = Boolean(plugin.operation && phase !== 'failed')
38
35
  const enabled = plugin.status !== 'disabled'
39
- const failed = plugin.status === 'failed' || browserError !== null
40
- const sourceError = plugin.error ? selectL4LocalizedText(plugin.error.message, locale) : null
41
- const status = unfinished
42
- ? t(`phase_${phase}`)
43
- : failed
44
- ? t('notLoaded')
45
- : enabled
46
- ? t('enabled')
47
- : t('disabled')
48
36
  return (
49
- <div className="space-y-2">
50
- <div className="flex flex-wrap items-center gap-2">
51
- {unfinished || pending || failed ? (
52
- <span
53
- role="status"
54
- className={`inline-flex items-center gap-1.5 rounded-md bg-muted px-2 py-1 text-xs ${failed && !unfinished ? 'text-destructive' : 'text-muted-foreground'}`}
55
- >
56
- {unfinished && phase !== 'waiting' ? (
57
- <LoaderCircleIcon className="size-3.5 animate-spin" />
58
- ) : null}
59
- {pending && !unfinished ? t('submitting') : status}
60
- </span>
61
- ) : null}
62
- {plugin.kind === 'extension' ? (
63
- <span className="text-xs text-muted-foreground">{t('localExtension')}</span>
64
- ) : null}
37
+ <div className="flex shrink-0 flex-wrap items-center gap-3">
38
+ {pending ? (
39
+ <span
40
+ role="status"
41
+ className="inline-flex items-center gap-1.5 text-sm text-muted-foreground"
42
+ >
43
+ {phase !== 'waiting' ? <LoaderCircleIcon className="size-4 animate-spin" /> : null}
44
+ {t(phase && phase !== 'failed' ? `phase_${phase}` : 'submitting')}
45
+ </span>
46
+ ) : plugin.updateAvailable && plugin.kind !== 'extension' && phase !== 'failed' ? (
47
+ <Button size="sm" disabled={disabled} onClick={() => onAction('update')}>
48
+ {t('update')}
49
+ </Button>
50
+ ) : null}
51
+ {phase === 'failed' ? (
52
+ <Button
53
+ size="sm"
54
+ variant="outline"
55
+ disabled={disabled || pending}
56
+ onClick={() => onAction('retry')}
57
+ >
58
+ {t('retry')}
59
+ </Button>
60
+ ) : browserError && plugin.pluginName ? (
65
61
  <Button
66
- type="button"
67
62
  size="sm"
68
63
  variant="outline"
69
- role="switch"
70
- aria-checked={enabled}
64
+ disabled={disabled || browserRetryPending}
65
+ onClick={() => onAction('retry-browser')}
66
+ >
67
+ {t('retryView')}
68
+ </Button>
69
+ ) : null}
70
+ <label className="flex min-h-8 cursor-pointer items-center gap-2 text-sm text-muted-foreground">
71
+ <Switch
72
+ checked={enabled}
71
73
  aria-label={t('togglePlugin', { name: displayName })}
72
74
  disabled={disabled || pending}
73
- onClick={() => onAction(enabled ? 'disable' : 'enable')}
75
+ onCheckedChange={(checked) => onAction(checked ? 'enable' : 'disable')}
76
+ />
77
+ {t(enabled ? 'enabled' : 'disabled')}
78
+ </label>
79
+ {plugin.kind !== 'extension' ? (
80
+ <Button
81
+ size="icon-sm"
82
+ variant="ghost"
83
+ className="text-muted-foreground hover:text-destructive"
84
+ aria-label={t('removeNamed', { name: displayName })}
85
+ title={t('remove')}
86
+ disabled={disabled || pending}
87
+ onClick={() => onAction('remove')}
74
88
  >
75
- <span
76
- aria-hidden="true"
77
- className={`flex h-4 w-7 items-center rounded-full p-0.5 ${enabled ? 'bg-primary' : 'bg-muted-foreground'}`}
78
- >
79
- <span
80
- className={`size-3 rounded-full bg-primary-foreground transition-transform ${enabled ? 'translate-x-3' : ''}`}
81
- />
82
- </span>
83
- {t(enabled ? 'enabled' : 'disabled')}
89
+ <Trash2Icon />
84
90
  </Button>
85
- {plugin.updateAvailable && plugin.kind !== 'extension' ? (
86
- <Button
87
- size="sm"
88
- variant="outline"
89
- disabled={disabled || pending}
90
- onClick={() => onAction('update')}
91
- >
92
- {t('update')}
93
- </Button>
94
- ) : null}
95
- {plugin.kind !== 'extension' ? (
96
- <Button
97
- size="sm"
98
- variant="ghost"
99
- className="text-muted-foreground hover:text-destructive"
100
- disabled={disabled || pending}
101
- onClick={() => onAction('remove')}
102
- >
103
- <Trash2Icon data-icon="inline-start" />
104
- {t('remove')}
105
- </Button>
106
- ) : null}
107
- {phase === 'failed' ? (
108
- <Button
109
- size="sm"
110
- variant="outline"
111
- disabled={disabled || pending}
112
- onClick={() => onAction('retry')}
113
- >
114
- {t('retry')}
115
- </Button>
116
- ) : null}
117
- {browserError && plugin.pluginName ? (
118
- <Button
119
- size="sm"
120
- variant="outline"
121
- disabled={disabled || browserRetryPending}
122
- onClick={() => onAction('retry-browser')}
123
- >
124
- {t('retryView')}
125
- </Button>
126
- ) : null}
127
- <details className="text-sm">
128
- <summary className="cursor-pointer rounded-lg px-2 py-1.5 outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring">
129
- {t('more')}
130
- </summary>
131
- <Button
132
- size="sm"
133
- variant="ghost"
134
- className="mt-1"
135
- disabled={disabled || (pending && phase !== 'waiting')}
136
- aria-label={t('reloadSource', { name: displayName })}
137
- onClick={() => onAction('reload')}
138
- >
139
- {t('reloadOne')}
140
- </Button>
141
- </details>
142
- </div>
143
- {plugin.operation?.message ? (
91
+ ) : null}
92
+ </div>
93
+ )
94
+ }
95
+
96
+ export function L2PluginManagementFeedback({
97
+ plugin,
98
+ requestError,
99
+ browserError
100
+ }: {
101
+ plugin: L2PluginManagementItem
102
+ requestError?: string
103
+ browserError: string | null
104
+ }): React.JSX.Element | null {
105
+ const { t } = useTranslation('pluginManagement')
106
+ const { locale } = useL4Region()
107
+ const phase = plugin.operation?.phase
108
+ const messages = [
109
+ requestError,
110
+ phase === 'failed' ? plugin.operation?.message : null,
111
+ plugin.error ? selectL4LocalizedText(plugin.error.message, locale) : null,
112
+ browserError,
113
+ plugin.updateError ? t('itemCheckFailed', { error: plugin.updateError }) : null
114
+ ].filter(
115
+ (message, index, all): message is string => Boolean(message) && all.indexOf(message) === index
116
+ )
117
+ if (!messages.length && phase !== 'waiting' && plugin.status !== 'failed') return null
118
+ return (
119
+ <div className="space-y-1">
120
+ {messages.map((message) => (
144
121
  <p
145
- className={`whitespace-pre-wrap text-sm [overflow-wrap:anywhere] ${phase === 'failed' ? 'text-destructive' : 'text-muted-foreground'}`}
122
+ key={message}
123
+ role="alert"
124
+ className="whitespace-pre-wrap text-sm text-destructive [overflow-wrap:anywhere]"
146
125
  >
126
+ {message}
127
+ </p>
128
+ ))}
129
+ {!messages.length && plugin.status === 'failed' ? (
130
+ <p className="text-sm text-destructive">{t('notLoaded')}</p>
131
+ ) : null}
132
+ {phase === 'waiting' && plugin.operation?.message ? (
133
+ <p role="status" className="text-sm text-muted-foreground [overflow-wrap:anywhere]">
147
134
  {plugin.operation.message}
148
135
  </p>
149
136
  ) : null}
150
- {[requestError, sourceError, browserError]
151
- .filter(
152
- (message, index, all) =>
153
- message && all.indexOf(message) === index && message !== plugin.operation?.message
154
- )
155
- .map((message) => (
156
- <p
157
- key={message}
158
- role="alert"
159
- className="whitespace-pre-wrap text-sm text-destructive [overflow-wrap:anywhere]"
160
- >
161
- {message}
162
- </p>
163
- ))}
164
137
  </div>
165
138
  )
166
139
  }