@jetcrab/pi-desk 1.1.1 → 1.1.5

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 +2 -2
  69. package/temp/build/pi-desk/release/.next/required-server-files.json +2 -2
  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-c6c68b33156be10a.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 → 1LdtGYUckBhYZy0R-6yG0}/_buildManifest.js +0 -0
  119. /package/temp/build/pi-desk/release/.next/static/{STVrBHA3z9KPtvBE4WcE2 → 1LdtGYUckBhYZy0R-6yG0}/_ssgManifest.js +0 -0
@@ -64,7 +64,7 @@ export function L2WorkbenchChatViewportManager({
64
64
  onSaveSnapshot,
65
65
  onClearSnapshot
66
66
  }: L2WorkbenchChatViewportManagerProps): null {
67
- const { scrollRef, scrollToBottom, state, stopScroll } = useStickToBottomContext()
67
+ const { isAtBottom, scrollRef, scrollToBottom, state, stopScroll } = useStickToBottomContext()
68
68
  const positioningRef = useRef(false)
69
69
  const initializedRef = useRef(false)
70
70
  const positionedUserAnchorRef = useRef<string | null>(null)
@@ -73,6 +73,19 @@ export function L2WorkbenchChatViewportManager({
73
73
  const suspendedSnapshotRef = useRef<L2WorkbenchChatViewportSnapshot | null>(null)
74
74
  const suspendedScrollTopRef = useRef<number | null>(null)
75
75
 
76
+ useLayoutEffect(() => {
77
+ const scrollElement = scrollRef.current
78
+ if (!scrollElement || !visible || !isAtBottom) return
79
+
80
+ // 同高度的内容换位不会触发 ResizeObserver,浏览器锚定产生的上移会被误判为用户上翻。
81
+ const previousOverflowAnchor = scrollElement.style.overflowAnchor
82
+ scrollElement.style.overflowAnchor = 'none'
83
+ logL3ConversationScroll('viewport:anchor-lock', scrollElement)
84
+ return () => {
85
+ scrollElement.style.overflowAnchor = previousOverflowAnchor
86
+ }
87
+ }, [isAtBottom, scrollRef, visible])
88
+
76
89
  useLayoutEffect(() => {
77
90
  const scrollElement = scrollRef.current
78
91
  if (!scrollElement) return
@@ -517,6 +517,38 @@ export function L2WorkbenchBranchPicker({
517
517
  if (childIndex >= 0) selectIndex(childIndex)
518
518
  }, [rows, selectIndex, selectedRow])
519
519
 
520
+ const handleEscape = useCallback((): void => {
521
+ if (confirmation) onCancelRunning()
522
+ else if (query) {
523
+ setQuery('')
524
+ setFoldedEntryIds(new Set())
525
+ scrollListTo(0)
526
+ } else requestClose(true)
527
+ }, [confirmation, onCancelRunning, query, requestClose, scrollListTo])
528
+
529
+ useEffect(() => {
530
+ if (!mobile || !present) return
531
+ const handleKeyDown = (event: KeyboardEvent): void => {
532
+ if (
533
+ event.key !== 'Escape' ||
534
+ event.defaultPrevented ||
535
+ event.isComposing ||
536
+ event.repeat ||
537
+ event.altKey ||
538
+ event.ctrlKey ||
539
+ event.metaKey ||
540
+ event.shiftKey ||
541
+ panelRef.current?.closest('[inert]')
542
+ )
543
+ return
544
+ event.preventDefault()
545
+ event.stopImmediatePropagation()
546
+ handleEscape()
547
+ }
548
+ window.addEventListener('keydown', handleKeyDown)
549
+ return () => window.removeEventListener('keydown', handleKeyDown)
550
+ }, [handleEscape, mobile, present])
551
+
520
552
  const handleSearchKeyDown = (event: ReactKeyboardEvent<HTMLInputElement>): void => {
521
553
  if (event.nativeEvent.isComposing) return
522
554
  event.stopPropagation()
@@ -561,11 +593,7 @@ export function L2WorkbenchBranchPicker({
561
593
  return
562
594
  case 'Escape':
563
595
  event.preventDefault()
564
- if (query) {
565
- setQuery('')
566
- setFoldedEntryIds(new Set())
567
- scrollListTo(0)
568
- } else requestClose(true)
596
+ handleEscape()
569
597
  return
570
598
  }
571
599
  }
@@ -67,7 +67,9 @@ export function L2WorkbenchSidebarLayout({
67
67
  >
68
68
  {sidebar}
69
69
  </div>
70
- {children}
70
+ <div className="flex min-h-0 min-w-0 flex-1" inert={open || undefined}>
71
+ {children}
72
+ </div>
71
73
  </>
72
74
  )
73
75
  }
@@ -1343,8 +1343,7 @@ export function L2Workbench({
1343
1343
  event.ctrlKey ||
1344
1344
  event.metaKey ||
1345
1345
  event.shiftKey ||
1346
- event.isComposing ||
1347
- branchPicker !== null
1346
+ event.isComposing
1348
1347
  ) {
1349
1348
  return
1350
1349
  }
@@ -1356,9 +1355,27 @@ export function L2Workbench({
1356
1355
  return
1357
1356
  }
1358
1357
 
1358
+ if (branchPicker !== null) return
1359
1359
  const workId = focusedWorkIdRef.current
1360
1360
  if (!workId) return
1361
1361
 
1362
+ if (mobile) {
1363
+ branchPickerShortcutLastEscapeRef.current = 0
1364
+ const workspace = fileRuntime.getWorkspace(workId)
1365
+ const column = document.querySelector(`[data-work-id="${CSS.escape(workId)}"]`)
1366
+ // 手机返回可能落在 BODY;隐藏在终端或侧栏下的文件不能响应。
1367
+ if (
1368
+ workspace?.windowOpen &&
1369
+ workspace.activePane === 'files' &&
1370
+ column &&
1371
+ !column.closest('[inert]')
1372
+ ) {
1373
+ event.preventDefault()
1374
+ fileRuntime.hideWindow(workId)
1375
+ }
1376
+ return
1377
+ }
1378
+
1362
1379
  const now = Date.now()
1363
1380
  if (now - branchPickerShortcutLastEscapeRef.current >= BRANCH_PICKER_SHORTCUT_INTERVAL_MS) {
1364
1381
  branchPickerShortcutLastEscapeRef.current = now
@@ -1372,7 +1389,7 @@ export function L2Workbench({
1372
1389
 
1373
1390
  window.addEventListener('keydown', handleKeyDown)
1374
1391
  return () => window.removeEventListener('keydown', handleKeyDown)
1375
- }, [branchPicker, closeMobileDrawer, mobile, openBranchPicker])
1392
+ }, [branchPicker, closeMobileDrawer, fileRuntime, mobile, openBranchPicker])
1376
1393
 
1377
1394
  useEffect(
1378
1395
  () =>
@@ -29,7 +29,6 @@ export function L3ConversationCallSummary({
29
29
  const { t } = useTranslation('conversation')
30
30
  const [open, setOpen] = useState(false)
31
31
  const [hoverBlocked, setHoverBlocked] = useState(false)
32
- const trigger = useRef<HTMLButtonElement>(null)
33
32
  const popup = useRef<HTMLDivElement>(null)
34
33
  const hoverPreview = useRef(false)
35
34
 
@@ -63,7 +62,6 @@ export function L3ConversationCallSummary({
63
62
  }}
64
63
  >
65
64
  <PopoverTrigger
66
- ref={trigger}
67
65
  openOnHover={!hoverBlocked}
68
66
  delay={250}
69
67
  closeDelay={180}
@@ -77,7 +75,6 @@ export function L3ConversationCallSummary({
77
75
  align="start"
78
76
  side="top"
79
77
  initialFocus={() => (hoverPreview.current ? false : true)}
80
- finalFocus={() => (hoverPreview.current ? false : trigger.current)}
81
78
  className={cn(styles.panel, 'w-[30rem] gap-0 p-2')}
82
79
  >
83
80
  <div className="flex shrink-0 items-center gap-2 border-b px-1 pb-1">
@@ -19,6 +19,7 @@ export function L3FileContextMenuContent({ children }: { children: ReactNode }):
19
19
  >
20
20
  <ContextMenuPrimitive.Popup
21
21
  data-slot="context-menu-content"
22
+ finalFocus={false}
22
23
  className={cn(l4MenuPopupClassName, 'flex flex-col overflow-hidden p-0')}
23
24
  >
24
25
  <L4ScrollArea className="min-h-0" viewportClassName="p-1">
@@ -68,14 +68,16 @@ function L4AppDialogContent({
68
68
  children,
69
69
  showCloseButton = true,
70
70
  finalFocus = false,
71
+ backdropClassName,
71
72
  ...props
72
73
  }: DialogPrimitive.Popup.Props & {
73
74
  showCloseButton?: boolean
75
+ backdropClassName?: string
74
76
  }): React.JSX.Element {
75
77
  const { t } = useTranslation('common')
76
78
  return (
77
79
  <L4AppDialogPortal>
78
- <L4AppDialogBackdrop />
80
+ <L4AppDialogBackdrop className={backdropClassName} forceRender={Boolean(backdropClassName)} />
79
81
  <DialogPrimitive.Popup
80
82
  data-slot="app-dialog-content"
81
83
  finalFocus={finalFocus}
@@ -209,6 +209,26 @@ export function L4ImageViewer(props: L4ImageViewerProps): React.JSX.Element | nu
209
209
  let viewer: L4ViewerRuntime | null = null
210
210
  let resizeFrame = 0
211
211
  let resizeObserver: ResizeObserver | null = null
212
+ const handleEscape = (event: KeyboardEvent): void => {
213
+ if (
214
+ event.key !== 'Escape' ||
215
+ event.defaultPrevented ||
216
+ event.isComposing ||
217
+ event.altKey ||
218
+ event.ctrlKey ||
219
+ event.metaKey ||
220
+ event.shiftKey ||
221
+ !viewer?.fulled
222
+ )
223
+ return
224
+ // Viewer.js 自身关闭时不 preventDefault,且会与下层弹窗同时响应。
225
+ event.preventDefault()
226
+ event.stopImmediatePropagation()
227
+ if (!event.repeat) {
228
+ viewerRoot(instanceClass)?.querySelector<HTMLElement>('.viewer-button')?.click()
229
+ }
230
+ }
231
+ window.addEventListener('keydown', handleEscape, true)
212
232
  const initialize = (): void => {
213
233
  if (disposed) return
214
234
  const initialIndex = initialIndexRef.current
@@ -299,6 +319,7 @@ export function L4ImageViewer(props: L4ImageViewerProps): React.JSX.Element | nu
299
319
 
300
320
  return () => {
301
321
  disposed = true
322
+ window.removeEventListener('keydown', handleEscape, true)
302
323
  resizeObserver?.disconnect()
303
324
  cancelAnimationFrame(resizeFrame)
304
325
  if (viewerRef.current === viewer) viewerRef.current = null
@@ -0,0 +1,27 @@
1
+ 'use client'
2
+
3
+ import { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox'
4
+ import { cn } from '@client/l4_foundation/lib/l4-utils'
5
+ import { CheckIcon } from 'lucide-react'
6
+
7
+ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
8
+ return (
9
+ <CheckboxPrimitive.Root
10
+ data-slot="checkbox"
11
+ className={cn(
12
+ 'peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary dark:data-checked:bg-primary',
13
+ className
14
+ )}
15
+ {...props}
16
+ >
17
+ <CheckboxPrimitive.Indicator
18
+ data-slot="checkbox-indicator"
19
+ className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
20
+ >
21
+ <CheckIcon />
22
+ </CheckboxPrimitive.Indicator>
23
+ </CheckboxPrimitive.Root>
24
+ )
25
+ }
26
+
27
+ export { Checkbox }
@@ -34,6 +34,7 @@ function ContextMenuContent({
34
34
  alignOffset = 4,
35
35
  side = 'right',
36
36
  sideOffset = 0,
37
+ finalFocus = false,
37
38
  ...props
38
39
  }: ContextMenuPrimitive.Popup.Props &
39
40
  Pick<ContextMenuPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset'>) {
@@ -48,6 +49,7 @@ function ContextMenuContent({
48
49
  >
49
50
  <ContextMenuPrimitive.Popup
50
51
  data-slot="context-menu-content"
52
+ finalFocus={finalFocus}
51
53
  className={cn(l4MenuPopupClassName, className)}
52
54
  {...props}
53
55
  />
@@ -134,10 +136,15 @@ function ContextMenuSubTrigger({
134
136
  )
135
137
  }
136
138
 
137
- function ContextMenuSubContent({ ...props }: React.ComponentProps<typeof ContextMenuContent>) {
139
+ // 子菜单关闭时上级菜单仍打开,需保留其键盘导航焦点。
140
+ function ContextMenuSubContent({
141
+ finalFocus = true,
142
+ ...props
143
+ }: React.ComponentProps<typeof ContextMenuContent>) {
138
144
  return (
139
145
  <ContextMenuContent
140
146
  data-slot="context-menu-sub-content"
147
+ finalFocus={finalFocus}
141
148
  className="min-w-40"
142
149
  side="right"
143
150
  {...props}
@@ -29,6 +29,7 @@ function DropdownMenuContent({
29
29
  side = 'bottom',
30
30
  sideOffset = 4,
31
31
  className,
32
+ finalFocus = false,
32
33
  ...props
33
34
  }: MenuPrimitive.Popup.Props &
34
35
  Pick<MenuPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset'>) {
@@ -43,6 +44,7 @@ function DropdownMenuContent({
43
44
  >
44
45
  <MenuPrimitive.Popup
45
46
  data-slot="dropdown-menu-content"
47
+ finalFocus={finalFocus}
46
48
  className={cn(l4MenuPopupClassName, className)}
47
49
  {...props}
48
50
  />
@@ -129,17 +131,20 @@ function DropdownMenuSubTrigger({
129
131
  )
130
132
  }
131
133
 
134
+ // 子菜单关闭时上级菜单仍打开,需保留其键盘导航焦点。
132
135
  function DropdownMenuSubContent({
133
136
  align = 'start',
134
137
  alignOffset = -3,
135
138
  side = 'right',
136
139
  sideOffset = 0,
137
140
  className,
141
+ finalFocus = true,
138
142
  ...props
139
143
  }: React.ComponentProps<typeof DropdownMenuContent>) {
140
144
  return (
141
145
  <DropdownMenuContent
142
146
  data-slot="dropdown-menu-sub-content"
147
+ finalFocus={finalFocus}
143
148
  className={cn('w-auto min-w-40', className)}
144
149
  align={align}
145
150
  alignOffset={alignOffset}
@@ -20,6 +20,7 @@ function PopoverContent({
20
20
  alignOffset = 0,
21
21
  side = 'bottom',
22
22
  sideOffset = 4,
23
+ finalFocus = false,
23
24
  ...props
24
25
  }: PopoverPrimitive.Popup.Props &
25
26
  Pick<PopoverPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset'> & {
@@ -36,6 +37,7 @@ function PopoverContent({
36
37
  >
37
38
  <PopoverPrimitive.Popup
38
39
  data-slot="popover-content"
40
+ finalFocus={finalFocus}
39
41
  className={cn(
40
42
  'z-50 flex w-80 max-w-[calc(100vw-2rem)] origin-(--transform-origin) flex-col gap-3 rounded-lg border border-border bg-popover p-3 text-sm text-popover-foreground shadow-md outline-hidden duration-100 motion-reduce:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
41
43
  className
@@ -63,6 +63,7 @@ function SelectContent({
63
63
  align = 'center',
64
64
  alignOffset = 0,
65
65
  alignItemWithTrigger = true,
66
+ finalFocus = false,
66
67
  ...props
67
68
  }: SelectPrimitive.Popup.Props &
68
69
  Pick<
@@ -83,6 +84,7 @@ function SelectContent({
83
84
  >
84
85
  <SelectPrimitive.Popup
85
86
  data-slot="select-content"
87
+ finalFocus={finalFocus}
86
88
  data-align-trigger={alignItemWithTrigger}
87
89
  className={cn(
88
90
  'relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
@@ -0,0 +1,31 @@
1
+ 'use client'
2
+
3
+ import { Switch as SwitchPrimitive } from '@base-ui/react/switch'
4
+ import { cn } from '@client/l4_foundation/lib/l4-utils'
5
+
6
+ function Switch({
7
+ className,
8
+ size = 'default',
9
+ ...props
10
+ }: SwitchPrimitive.Root.Props & {
11
+ size?: 'sm' | 'default'
12
+ }) {
13
+ return (
14
+ <SwitchPrimitive.Root
15
+ data-slot="switch"
16
+ data-size={size}
17
+ className={cn(
18
+ 'peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none group-has-[:focus-visible]/field-label:border-transparent group-has-[:focus-visible]/field-label:ring-0 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50',
19
+ className
20
+ )}
21
+ {...props}
22
+ >
23
+ <SwitchPrimitive.Thumb
24
+ data-slot="switch-thumb"
25
+ className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
26
+ />
27
+ </SwitchPrimitive.Root>
28
+ )
29
+ }
30
+
31
+ export { Switch }
@@ -0,0 +1,74 @@
1
+ 'use client'
2
+
3
+ import { Tabs as TabsPrimitive } from '@base-ui/react/tabs'
4
+ import { cva, type VariantProps } from 'class-variance-authority'
5
+ import { cn } from '@client/l4_foundation/lib/l4-utils'
6
+
7
+ function Tabs({ className, orientation = 'horizontal', ...props }: TabsPrimitive.Root.Props) {
8
+ return (
9
+ <TabsPrimitive.Root
10
+ data-slot="tabs"
11
+ data-orientation={orientation}
12
+ className={cn('group/tabs flex gap-2 data-horizontal:flex-col', className)}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ const tabsListVariants = cva(
19
+ 'group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none',
20
+ {
21
+ variants: {
22
+ variant: {
23
+ default: 'bg-muted',
24
+ line: 'gap-1 bg-transparent'
25
+ }
26
+ },
27
+ defaultVariants: {
28
+ variant: 'default'
29
+ }
30
+ }
31
+ )
32
+
33
+ function TabsList({
34
+ className,
35
+ variant = 'default',
36
+ ...props
37
+ }: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) {
38
+ return (
39
+ <TabsPrimitive.List
40
+ data-slot="tabs-list"
41
+ data-variant={variant}
42
+ className={cn(tabsListVariants({ variant }), className)}
43
+ {...props}
44
+ />
45
+ )
46
+ }
47
+
48
+ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
49
+ return (
50
+ <TabsPrimitive.Tab
51
+ data-slot="tabs-trigger"
52
+ className={cn(
53
+ "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
54
+ 'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',
55
+ 'data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground',
56
+ 'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100',
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
65
+ return (
66
+ <TabsPrimitive.Panel
67
+ data-slot="tabs-content"
68
+ className={cn('flex-1 text-sm outline-none', className)}
69
+ {...props}
70
+ />
71
+ )
72
+ }
73
+
74
+ export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
@@ -35,8 +35,8 @@ export const L2_PLUGIN_MANAGEMENT_MESSAGES = {
35
35
  title: 'Plugins',
36
36
  description: 'Manage plugin packages and local extensions.',
37
37
  noPluginDescription: 'No description available',
38
- checkUpdates: 'Check plugin updates',
39
- checkingUpdates: 'Check for updates',
38
+ checkUpdates: 'Check updates',
39
+ checkingUpdates: 'Checking…',
40
40
  restartApply: 'Restart to finish',
41
41
  installPlugin: 'Install plugin',
42
42
  source: 'Plugin source',
@@ -135,7 +135,21 @@ export const L2_PLUGIN_MANAGEMENT_MESSAGES = {
135
135
  loadingFullDetails: 'Loading full capability details',
136
136
  installedTab: 'Installed',
137
137
  searchPlugins: 'Find plugins',
138
- directInstall: 'Add plugins',
138
+ directInstall: 'Manual install',
139
+ advancedOptions: 'Advanced options',
140
+ capabilityDetails: 'Features and runtime details',
141
+ newVersion: 'New v{{version}}',
142
+ removeNamed: 'Uninstall {{name}}',
143
+ updateAll: 'Update all ({{count}})',
144
+ batchManage: 'Select plugins',
145
+ finishSelection: 'Done',
146
+ selectAll: 'Select all',
147
+ filterStatus: 'Plugin status',
148
+ statusFilter_all: 'All statuses',
149
+ statusFilter_updates: 'Updates available',
150
+ statusFilter_disabled: 'Disabled',
151
+ statusFilter_failed: 'Errors',
152
+ batchWaiting: 'Not submitted. Finish pending operations, then try again.',
139
153
  batchInstall: 'Install plugins',
140
154
  batchSourcePlaceholder:
141
155
  'One npm package per line; a single source can also be a Git URL or local directory',
@@ -143,7 +157,7 @@ export const L2_PLUGIN_MANAGEMENT_MESSAGES = {
143
157
  batchLimit: 'Choose up to 32 plugins per batch.',
144
158
  batchFailed: 'Could not submit plugin changes',
145
159
  sourceBusy: 'This plugin has an operation in progress. Try again when it finishes.',
146
- selectFiltered: 'Select all filtered',
160
+ selectFiltered: 'Select all results',
147
161
  selectPlugin: 'Select {{name}}',
148
162
  selectedCount: '{{count}} selected',
149
163
  clearSelection: 'Clear selection',
@@ -151,17 +165,17 @@ export const L2_PLUGIN_MANAGEMENT_MESSAGES = {
151
165
  updateSelected: 'Update selected',
152
166
  removeSelected: 'Uninstall selected',
153
167
  setChannel: 'Set channel',
154
- installChannel: 'Update channel',
155
- defaultChannel: 'Default channel',
168
+ installChannel: 'Version preference',
169
+ defaultChannel: 'Default',
156
170
  automaticVersion: 'Default version',
157
171
  allChannels: 'All channels',
158
172
  filterChannel: 'Filter by channel',
159
- channelLatest: 'latest · Stable',
160
- channelDev: 'dev · Development',
173
+ channelLatest: 'Stable',
174
+ channelDev: 'Development',
161
175
  followingChannel: 'Channel: {{tag}}',
162
176
  targetVersion: 'Target: {{version}}',
163
177
  itemCheckFailed: 'Update check failed: {{error}}',
164
- filterInstalled: 'Filter installed plugins',
178
+ filterInstalled: 'Search installed plugins',
165
179
  noMatches: 'No matching plugins',
166
180
  enabled: 'Enabled',
167
181
  disabled: 'Disabled',
@@ -176,6 +190,7 @@ export const L2_PLUGIN_MANAGEMENT_MESSAGES = {
176
190
  downloadSource: 'Download source',
177
191
  installDownloadSource: 'Download source for this install',
178
192
  useDefaultSource: 'Use saved default',
193
+ download_local: 'No override (local configuration)',
179
194
  download_auto: 'Automatic',
180
195
  download_domestic: 'China mirror',
181
196
  download_official: 'Official npm',
@@ -189,12 +204,12 @@ export const L2_PLUGIN_MANAGEMENT_MESSAGES = {
189
204
  invalidRegistry:
190
205
  'Enter a valid HTTP or HTTPS registry URL without credentials, query or fragment.',
191
206
  searchPlaceholder: 'Search plugin name or description',
192
- searchSource: 'Search source',
207
+ searchSource: 'Search registry',
193
208
  publicNpm: 'Public npm',
194
209
  customSearchSource: 'Custom registry',
195
210
  searchRegistryUrl: 'Search registry URL',
196
211
  pluginKind: 'Plugin type',
197
- kind_all: 'All',
212
+ kind_all: 'All types',
198
213
  kind_desk: 'Pi Desk',
199
214
  kind_pi: 'Pi',
200
215
  kind_unknown: 'Unverified type',
@@ -260,8 +275,8 @@ export const L2_PLUGIN_MANAGEMENT_MESSAGES = {
260
275
  title: '插件',
261
276
  description: '管理插件包与本地扩展。',
262
277
  noPluginDescription: '暂无说明',
263
- checkUpdates: '检查插件更新',
264
- checkingUpdates: '检查更新',
278
+ checkUpdates: '检查更新',
279
+ checkingUpdates: '检查中…',
265
280
  restartApply: '重启并完成',
266
281
  installPlugin: '安装插件',
267
282
  source: '插件来源',
@@ -357,14 +372,28 @@ export const L2_PLUGIN_MANAGEMENT_MESSAGES = {
357
372
  loadingFullDetails: '正在读取完整能力详情',
358
373
  installedTab: '已安装',
359
374
  searchPlugins: '搜索插件',
360
- directInstall: '添加插件',
375
+ directInstall: '手动安装',
376
+ advancedOptions: '高级选项',
377
+ capabilityDetails: '功能与运行信息',
378
+ newVersion: '新版本 v{{version}}',
379
+ removeNamed: '卸载 {{name}}',
380
+ updateAll: '全部更新({{count}})',
381
+ batchManage: '批量管理',
382
+ finishSelection: '完成',
383
+ selectAll: '全选',
384
+ filterStatus: '插件状态',
385
+ statusFilter_all: '全部状态',
386
+ statusFilter_updates: '有更新',
387
+ statusFilter_disabled: '已停用',
388
+ statusFilter_failed: '异常',
389
+ batchWaiting: '尚未提交,请完成等待中的操作后重试。',
361
390
  batchInstall: '批量安装',
362
391
  batchSourcePlaceholder: '每行一个 npm 包名;单项也可填写 Git 地址或本地目录',
363
392
  batchNpmOnly: '批量安装时请每行填写一个 npm 包名。',
364
393
  batchLimit: '每次批量操作最多选择 32 个插件。',
365
394
  batchFailed: '提交插件变更失败',
366
395
  sourceBusy: '此插件正在处理其他操作,完成后可重试。',
367
- selectFiltered: '选择当前筛选全部',
396
+ selectFiltered: '全选当前结果',
368
397
  selectPlugin: '选择 {{name}}',
369
398
  selectedCount: '已选 {{count}} 项',
370
399
  clearSelection: '清空选择',
@@ -372,17 +401,17 @@ export const L2_PLUGIN_MANAGEMENT_MESSAGES = {
372
401
  updateSelected: '更新所选',
373
402
  removeSelected: '卸载所选',
374
403
  setChannel: '设置渠道',
375
- installChannel: '更新渠道',
376
- defaultChannel: '默认渠道',
404
+ installChannel: '版本偏好',
405
+ defaultChannel: '默认',
377
406
  automaticVersion: '默认版本',
378
407
  allChannels: '全部渠道',
379
408
  filterChannel: '按渠道筛选',
380
- channelLatest: 'latest · 稳定版',
381
- channelDev: 'dev · 开发版',
409
+ channelLatest: '稳定版',
410
+ channelDev: '开发版',
382
411
  followingChannel: '跟随 {{tag}}',
383
412
  targetVersion: '目标版本 {{version}}',
384
413
  itemCheckFailed: '查询更新失败:{{error}}',
385
- filterInstalled: '筛选已安装插件',
414
+ filterInstalled: '搜索已安装插件',
386
415
  noMatches: '没有匹配的插件',
387
416
  enabled: '已启用',
388
417
  disabled: '已停用',
@@ -397,6 +426,7 @@ export const L2_PLUGIN_MANAGEMENT_MESSAGES = {
397
426
  downloadSource: '下载源',
398
427
  installDownloadSource: '本次安装下载源',
399
428
  useDefaultSource: '使用已保存的默认设置',
429
+ download_local: '不使用源(本机配置)',
400
430
  download_auto: '自动',
401
431
  download_domestic: '国内加速',
402
432
  download_official: '官方',
@@ -409,12 +439,12 @@ export const L2_PLUGIN_MANAGEMENT_MESSAGES = {
409
439
  settingsFailed: '保存或读取下载设置失败',
410
440
  invalidRegistry: '请输入有效的 HTTP 或 HTTPS 仓库地址,不包含账号、查询参数或片段。',
411
441
  searchPlaceholder: '搜索插件名称或简介',
412
- searchSource: '搜索来源',
442
+ searchSource: '搜索仓库',
413
443
  publicNpm: 'npm 公开仓库',
414
444
  customSearchSource: '自定义仓库',
415
445
  searchRegistryUrl: '搜索仓库地址',
416
446
  pluginKind: '插件类型',
417
- kind_all: '全部',
447
+ kind_all: '全部类型',
418
448
  kind_desk: 'Pi Desk',
419
449
  kind_pi: 'Pi',
420
450
  kind_unknown: '类型待确认',
@@ -76,7 +76,7 @@ export const L3_CONVERSATION_LOCALE_MESSAGES = {
76
76
  mode: 'Mode',
77
77
  cwd: 'Project directory',
78
78
  agentDir: 'Pi directory',
79
- skillDirectory: 'Skill directory'
79
+ docsDirectory: 'Documentation directory'
80
80
  },
81
81
  environment: { development: 'Development', production: 'Production' },
82
82
  mode: { basic: 'Basic mode', normal: 'Normal mode' },
@@ -221,7 +221,7 @@ export const L3_CONVERSATION_LOCALE_MESSAGES = {
221
221
  mode: '运行模式',
222
222
  cwd: '项目目录',
223
223
  agentDir: 'Pi 目录',
224
- skillDirectory: 'Skill 目录'
224
+ docsDirectory: '文档目录'
225
225
  },
226
226
  environment: { development: '开发环境', production: '正式环境' },
227
227
  mode: { basic: '基础模式', normal: '正常模式' },