@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
@@ -18,137 +18,115 @@ import {
18
18
  } from '../l2-use-plugin-management'
19
19
 
20
20
  interface ManagementDialogProps {
21
- open: boolean
22
- title: string
23
21
  confirmation: PluginManagementAction | null
24
22
  onClose: () => void
25
23
  onConfirm: (action: PluginManagementAction) => void
26
- children: React.ReactNode
27
24
  }
28
25
 
29
26
  export function L2PluginManagementDialog({
30
- open,
31
- title,
32
27
  confirmation,
33
28
  onClose,
34
- onConfirm,
35
- children
29
+ onConfirm
36
30
  }: ManagementDialogProps): React.JSX.Element {
37
31
  const { t } = useTranslation('pluginManagement')
38
32
  const action = confirmation ? t(pluginManagementActionLabel(confirmation)) : null
33
+ const removing =
34
+ confirmation?.kind === 'del' ||
35
+ (confirmation?.kind === 'batch' && confirmation.input.action === 'del')
36
+ const installing =
37
+ confirmation?.kind === 'add' ||
38
+ (confirmation?.kind === 'batch' && confirmation.input.action === 'add')
39
39
  return (
40
40
  <L4AppDialogRoot
41
- open={open}
41
+ open={Boolean(confirmation)}
42
42
  onOpenChange={(value) => {
43
43
  if (!value) onClose()
44
44
  }}
45
45
  >
46
- <L4AppDialogContent
47
- finalFocus
48
- className={confirmation ? 'max-w-lg' : 'h-[min(85dvh,52rem)] max-w-3xl'}
49
- >
46
+ <L4AppDialogContent className="z-[150] max-w-lg" backdropClassName="z-[140]">
50
47
  <L4AppDialogHeader className="p-4 pr-12">
51
- <L4AppDialogTitle className="min-w-0 [overflow-wrap:anywhere]">
52
- {confirmation ? t('confirmAction', { action }) : title}
53
- </L4AppDialogTitle>
48
+ <L4AppDialogTitle>{t('confirmAction', { action })}</L4AppDialogTitle>
54
49
  <L4AppDialogDescription>
55
- {confirmation
56
- ? confirmation.kind === 'del' ||
57
- (confirmation.kind === 'batch' && confirmation.input.action === 'del')
58
- ? t('removeDescription')
59
- : confirmation.kind === 'add' ||
60
- (confirmation.kind === 'batch' && confirmation.input.action === 'add')
61
- ? t('thirdPartyDescription')
62
- : confirmation.kind === 'reload'
63
- ? t(
64
- confirmation.mode === 'normal'
65
- ? 'restartNormalDescription'
66
- : 'restartDescription'
67
- )
68
- : t('confirmChange')
69
- : t('pluginDetails')}
50
+ {removing
51
+ ? t('removeDescription')
52
+ : installing
53
+ ? t('thirdPartyDescription')
54
+ : confirmation?.kind === 'reload'
55
+ ? t(
56
+ confirmation.mode === 'normal'
57
+ ? 'restartNormalDescription'
58
+ : 'restartDescription'
59
+ )
60
+ : t('confirmChange')}
70
61
  </L4AppDialogDescription>
71
62
  </L4AppDialogHeader>
72
63
  <L4AppDialogBody className="min-h-0">
73
64
  <div className="min-w-0 p-4 pt-0">
74
- {confirmation ? (
75
- confirmation.kind === 'batch' ? (
76
- <ul className="space-y-2 text-sm">
77
- {(confirmation.input.action === 'add'
78
- ? confirmation.input.items.map((item) => item.source)
79
- : confirmation.input.sources
80
- ).map((source) => (
81
- <li key={source} className="font-medium [overflow-wrap:anywhere]">
82
- {source}
83
- </li>
84
- ))}
85
- </ul>
86
- ) : confirmation.kind === 'add' ? (
87
- <dl className="space-y-3 text-sm">
88
- <div>
89
- <dt className="text-muted-foreground">{t('packageName')}</dt>
90
- <dd className="mt-1 font-medium [overflow-wrap:anywhere]">
91
- {confirmation.package?.name ?? confirmation.input.source}
92
- </dd>
93
- </div>
94
- <div>
95
- <dt className="text-muted-foreground">{t('version')}</dt>
96
- <dd className="[overflow-wrap:anywhere]">
97
- {confirmation.package?.version ??
98
- pluginNpmSpec(confirmation.input.source)?.version ??
99
- t('fromSource')}
100
- </dd>
101
- </div>
102
- {confirmation.input.tag ? (
103
- <div>
104
- <dt className="text-muted-foreground">{t('installChannel')}</dt>
105
- <dd>{confirmation.input.tag}</dd>
106
- </div>
107
- ) : null}
108
- <div>
109
- <dt className="text-muted-foreground">{t('publisher')}</dt>
110
- <dd className="[overflow-wrap:anywhere]">
111
- {confirmation.package?.publisher ?? t('unknownPublisher')}
112
- </dd>
113
- </div>
114
- <div>
115
- <dt className="text-muted-foreground">{t('source')}</dt>
116
- <dd className="[overflow-wrap:anywhere]">
117
- {confirmation.input.registry ??
118
- confirmation.package?.registry ??
119
- confirmation.input.source}
120
- </dd>
121
- </div>
122
- </dl>
123
- ) : confirmation.kind !== 'reload' ? (
124
- <p className="text-sm font-medium [overflow-wrap:anywhere]">
125
- {confirmation.source}
126
- </p>
127
- ) : null
128
- ) : (
129
- children
130
- )}
65
+ {confirmation?.kind === 'batch' ? (
66
+ <ul className="space-y-2 text-sm">
67
+ {(confirmation.input.action === 'add'
68
+ ? confirmation.input.items.map((item) => item.source)
69
+ : confirmation.input.sources
70
+ ).map((source) => (
71
+ <li key={source} className="font-medium [overflow-wrap:anywhere]">
72
+ {source.startsWith('npm:') ? (pluginNpmSpec(source)?.name ?? source) : source}
73
+ </li>
74
+ ))}
75
+ </ul>
76
+ ) : confirmation?.kind === 'add' ? (
77
+ <dl className="space-y-3 text-sm">
78
+ <div>
79
+ <dt className="text-muted-foreground">{t('packageName')}</dt>
80
+ <dd className="mt-1 font-medium [overflow-wrap:anywhere]">
81
+ {confirmation.package?.name ?? confirmation.input.source}
82
+ </dd>
83
+ </div>
84
+ <div>
85
+ <dt className="text-muted-foreground">{t('version')}</dt>
86
+ <dd className="[overflow-wrap:anywhere]">
87
+ {confirmation.package?.version ??
88
+ pluginNpmSpec(confirmation.input.source)?.version ??
89
+ t('fromSource')}
90
+ </dd>
91
+ </div>
92
+ <div>
93
+ <dt className="text-muted-foreground">{t('publisher')}</dt>
94
+ <dd className="[overflow-wrap:anywhere]">
95
+ {confirmation.package?.publisher ?? t('unknownPublisher')}
96
+ </dd>
97
+ </div>
98
+ <div>
99
+ <dt className="text-muted-foreground">{t('source')}</dt>
100
+ <dd className="[overflow-wrap:anywhere]">
101
+ {confirmation.input.registry ??
102
+ confirmation.package?.registry ??
103
+ confirmation.input.source}
104
+ </dd>
105
+ </div>
106
+ </dl>
107
+ ) : confirmation && confirmation.kind !== 'reload' ? (
108
+ <p className="text-sm font-medium [overflow-wrap:anywhere]">
109
+ {confirmation.source.startsWith('npm:')
110
+ ? (pluginNpmSpec(confirmation.source)?.name ?? confirmation.source)
111
+ : confirmation.source}
112
+ </p>
113
+ ) : null}
131
114
  </div>
132
115
  </L4AppDialogBody>
133
- {confirmation ? (
134
- <L4AppDialogFooter>
135
- <Button size="sm" variant="outline" onClick={onClose}>
136
- {t('cancel')}
137
- </Button>
138
- <Button
139
- size="sm"
140
- variant={
141
- confirmation.kind === 'del' ||
142
- (confirmation.kind === 'batch' && confirmation.input.action === 'del')
143
- ? 'destructive'
144
- : 'default'
145
- }
146
- onClick={() => onConfirm(confirmation)}
147
- >
148
- {action}
149
- </Button>
150
- </L4AppDialogFooter>
151
- ) : null}
116
+ <L4AppDialogFooter>
117
+ <Button size="sm" variant="outline" onClick={onClose}>
118
+ {t('cancel')}
119
+ </Button>
120
+ <Button
121
+ size="sm"
122
+ variant={removing ? 'destructive' : 'default'}
123
+ onClick={() => {
124
+ if (confirmation) onConfirm(confirmation)
125
+ }}
126
+ >
127
+ {action}
128
+ </Button>
129
+ </L4AppDialogFooter>
152
130
  </L4AppDialogContent>
153
131
  </L4AppDialogRoot>
154
132
  )
@@ -8,6 +8,13 @@ import {
8
8
  } from '@common/l4_foundation/locale/l4-localized-text'
9
9
  import { useL4Region } from '@client/l4_foundation/locale/l4-region-provider'
10
10
  import { MessageResponse } from '@client/l4_foundation/ui/ai-elements/message'
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue
17
+ } from '@client/l4_foundation/ui/shadcn/select'
11
18
 
12
19
  export function L2PluginReadme({ readme }: { readme: L4LocalizedText | null }): React.JSX.Element {
13
20
  const { t } = useTranslation('pluginManagement')
@@ -26,21 +33,28 @@ export function L2PluginReadme({ readme }: { readme: L4LocalizedText | null }):
26
33
  <div className="flex flex-wrap items-center justify-between gap-2">
27
34
  <h3 className="text-sm font-semibold">{t('readme')}</h3>
28
35
  {languages.length > 0 ? (
29
- <label className="flex items-center gap-2 text-sm">
30
- {t('readmeLanguage')}
31
- <select
32
- value={selected ?? ''}
33
- onChange={(event) => setLanguage(event.target.value || null)}
34
- className="min-h-8 rounded-lg border border-input bg-background px-2 outline-none focus-visible:ring-2 focus-visible:ring-ring"
35
- >
36
- <option value="">{t('defaultLanguage')}</option>
37
- {languages.map((key) => (
38
- <option key={key} value={key}>
39
- {key.startsWith('zh') ? '中文' : 'English'}
40
- </option>
41
- ))}
42
- </select>
43
- </label>
36
+ <div className="flex items-center gap-2 text-sm">
37
+ <span>{t('readmeLanguage')}</span>
38
+ <Select value={selected ?? ''} onValueChange={(value) => setLanguage(value || null)}>
39
+ <SelectTrigger aria-label={t('readmeLanguage')}>
40
+ <SelectValue>
41
+ {selected
42
+ ? selected.startsWith('zh')
43
+ ? '中文'
44
+ : 'English'
45
+ : t('defaultLanguage')}
46
+ </SelectValue>
47
+ </SelectTrigger>
48
+ <SelectContent positionerClassName="z-[160]">
49
+ <SelectItem value="">{t('defaultLanguage')}</SelectItem>
50
+ {languages.map((key) => (
51
+ <SelectItem key={key} value={key}>
52
+ {key.startsWith('zh') ? '中文' : 'English'}
53
+ </SelectItem>
54
+ ))}
55
+ </SelectContent>
56
+ </Select>
57
+ </div>
44
58
  ) : null}
45
59
  </div>
46
60
  {content ? (
@@ -24,6 +24,7 @@ import {
24
24
  interface ProjectModelDefaultSettingsState {
25
25
  directories: L2PiDirectoryListResponse['directories']
26
26
  cwd: string
27
+ loadedCwd: string | null
27
28
  models: L2ModelOption[]
28
29
  selection: L2ModelSelection | null
29
30
  fixed: boolean
@@ -66,9 +67,11 @@ export function useL2ProjectModelDefaultSettings(
66
67
  const [projectError, setProjectError] = useState<string | null>(null)
67
68
  const [directoryAttempt, setDirectoryAttempt] = useState(0)
68
69
  const [projectAttempt, setProjectAttempt] = useState(0)
69
- const projectReady = loadedCwd === cwd && projectError === null
70
+ const projectReady =
71
+ loadedCwd === cwd && projectError === null && !projectLoading && !projectRefreshing
70
72
  const projectDraftDirtyRef = useRef(false)
71
73
  const projectCwdRef = useRef('')
74
+ const projectRequestRef = useRef(0)
72
75
  const markDraftDirty = useCallback(
73
76
  (dirty: boolean): void => {
74
77
  projectDraftDirtyRef.current = dirty
@@ -137,12 +140,16 @@ export function useL2ProjectModelDefaultSettings(
137
140
 
138
141
  useEffect(() => {
139
142
  let disposed = false
143
+ const request = ++projectRequestRef.current
144
+ function isCurrent(): boolean {
145
+ return !disposed && request === projectRequestRef.current
146
+ }
140
147
  const projectChanged = projectCwdRef.current !== cwd
141
148
  projectCwdRef.current = cwd
142
149
  if (projectChanged) markDraftDirty(false)
143
150
  if (!cwd) {
144
151
  queueMicrotask((): void => {
145
- if (disposed) return
152
+ if (!isCurrent()) return
146
153
  setModels([])
147
154
  setSelection(null)
148
155
  setFixed(false)
@@ -158,18 +165,13 @@ export function useL2ProjectModelDefaultSettings(
158
165
 
159
166
  const cached = readL2ProjectModelDefaultCache(cwd)
160
167
  queueMicrotask((): void => {
161
- if (disposed) return
168
+ if (!isCurrent()) return
162
169
  setProjectError(null)
163
170
  // 同一项目重试或后台刷新时,缓存和远端结果都不能替换未保存草稿。
164
171
  if (!projectDraftDirtyRef.current) {
165
172
  if (cached) {
166
173
  applyProjectResult(cached)
167
174
  setLoadedCwd(cwd)
168
- } else {
169
- setModels([])
170
- setSelection(null)
171
- setFixed(false)
172
- setLoadedCwd(null)
173
175
  }
174
176
  }
175
177
  setProjectLoading(!cached && !projectDraftDirtyRef.current)
@@ -179,17 +181,17 @@ export function useL2ProjectModelDefaultSettings(
179
181
  void (async (): Promise<void> => {
180
182
  try {
181
183
  const result = await biz.getProjectDefault(cwd)
182
- if (disposed) return
184
+ if (!isCurrent()) return
183
185
  if (!projectDraftDirtyRef.current) applyProjectResult(result)
184
186
  setLoadedCwd(cwd)
185
187
  } catch (error) {
186
- if (!disposed) {
188
+ if (isCurrent()) {
187
189
  const message = error instanceof Error ? error.message : t('projectDefaultFailed')
188
190
  console.warn('[Pi Desk][ProjectModelDefault] 读取项目模型配置失败', { cwd, message })
189
191
  setProjectError(message)
190
192
  }
191
193
  } finally {
192
- if (!disposed) {
194
+ if (isCurrent()) {
193
195
  setProjectLoading(false)
194
196
  setProjectRefreshing(false)
195
197
  }
@@ -213,16 +215,20 @@ export function useL2ProjectModelDefaultSettings(
213
215
  ) {
214
216
  return false
215
217
  }
218
+ projectRequestRef.current += 1
219
+ setProjectRefreshing(true)
216
220
  setCwd(nextCwd)
217
221
  return true
218
222
  }
219
223
 
220
224
  function changeFixed(value: boolean): void {
225
+ if (!projectReady || saving) return
221
226
  markDraftDirty(true)
222
227
  setFixed(value)
223
228
  }
224
229
 
225
230
  function selectModel(model: L2ModelOption): void {
231
+ if (!projectReady || saving) return
226
232
  markDraftDirty(true)
227
233
  setSelection({
228
234
  provider: model.provider,
@@ -232,6 +238,7 @@ export function useL2ProjectModelDefaultSettings(
232
238
  }
233
239
 
234
240
  function selectThinking(value: string): void {
241
+ if (!projectReady || saving) return
235
242
  const model = models.find(
236
243
  (item): boolean => item.provider === selection?.provider && item.modelId === selection.modelId
237
244
  )
@@ -242,6 +249,7 @@ export function useL2ProjectModelDefaultSettings(
242
249
  }
243
250
 
244
251
  function applyPreset(preset: L2ModelPreset): void {
252
+ if (!projectReady || saving) return
245
253
  markDraftDirty(true)
246
254
  setSelection({
247
255
  provider: preset.provider,
@@ -255,6 +263,8 @@ export function useL2ProjectModelDefaultSettings(
255
263
  }
256
264
 
257
265
  function retryProject(): void {
266
+ projectRequestRef.current += 1
267
+ setProjectRefreshing(true)
258
268
  setProjectAttempt((attempt): number => attempt + 1)
259
269
  }
260
270
 
@@ -289,6 +299,7 @@ export function useL2ProjectModelDefaultSettings(
289
299
  return {
290
300
  directories,
291
301
  cwd,
302
+ loadedCwd,
292
303
  models,
293
304
  selection,
294
305
  fixed,
@@ -39,7 +39,6 @@ import {
39
39
  import { cn } from '@client/l4_foundation/lib/l4-utils'
40
40
  import { Button } from '@client/l4_foundation/ui/shadcn/button'
41
41
  import { L4BrandIcon } from '@client/l4_foundation/ui/l4-brand-icon'
42
- import { useL4WindowReveal } from '@client/l4_foundation/ui/l4-window-reveal'
43
42
  import {
44
43
  Collapsible,
45
44
  CollapsibleContent,
@@ -241,8 +240,6 @@ export function L2Settings({
241
240
  activeSection.startsWith('plugin:') && !activePluginPage ? 'appearance' : activeSection
242
241
  const activeBuiltinSection = SETTINGS_SECTIONS.find((section) => section.id === effectiveSection)
243
242
  const ActiveBuiltinIcon = activeBuiltinSection?.icon
244
- const contentRef = useRef<HTMLDivElement>(null)
245
- useL4WindowReveal(contentRef, open && !activePluginPage ? effectiveSection : null)
246
243
 
247
244
  const runAfterGuard = useCallback(async (action: () => void): Promise<void> => {
248
245
  if (guardPendingRef.current) return
@@ -475,7 +472,7 @@ export function L2Settings({
475
472
  />
476
473
  </div>
477
474
  ) : effectiveSection === 'skills' ? (
478
- <div ref={contentRef} className="min-h-0 flex-1 overflow-hidden">
475
+ <div className="min-h-0 flex-1 overflow-hidden">
479
476
  {open && (
480
477
  <L2SkillsSettings
481
478
  projects={skillProjects}
@@ -485,7 +482,7 @@ export function L2Settings({
485
482
  )}
486
483
  </div>
487
484
  ) : effectiveSection === 'mcp-settings' ? (
488
- <div ref={contentRef} className="min-h-0 min-w-0 flex-1 overflow-hidden">
485
+ <div className="min-h-0 min-w-0 flex-1 overflow-hidden">
489
486
  {open && (
490
487
  <ExternalSettingsSection
491
488
  render={renderMcpSettings}
@@ -494,7 +491,7 @@ export function L2Settings({
494
491
  )}
495
492
  </div>
496
493
  ) : effectiveSection === 'capability-modes' ? (
497
- <div ref={contentRef} className="min-h-0 min-w-0 flex-1 overflow-hidden">
494
+ <div className="min-h-0 min-w-0 flex-1 overflow-hidden">
498
495
  {open && (
499
496
  <ExternalSettingsSection
500
497
  render={renderCapabilityModesSettings}
@@ -503,7 +500,7 @@ export function L2Settings({
503
500
  )}
504
501
  </div>
505
502
  ) : effectiveSection === 'models' ? (
506
- <div ref={contentRef} className="min-h-0 flex-1 overflow-hidden">
503
+ <div className="min-h-0 flex-1 overflow-hidden">
507
504
  {open && (
508
505
  <L2ModelSettingsView
509
506
  connectionReady={connectionReady}
@@ -513,12 +510,10 @@ export function L2Settings({
513
510
  )}
514
511
  </div>
515
512
  ) : effectiveSection === 'plugins' ? (
516
- <div ref={contentRef} className="min-h-0 flex-1 overflow-hidden">
517
- {renderPluginManagement()}
518
- </div>
513
+ <div className="min-h-0 flex-1 overflow-hidden">{renderPluginManagement()}</div>
519
514
  ) : (
520
515
  <L4AppDialogBody className="min-w-0 max-h-none">
521
- <div ref={contentRef} className="p-4 sm:p-6">
516
+ <div className="p-4 sm:p-6">
522
517
  {effectiveSection === 'appearance' ? (
523
518
  <L2AppearanceSettings />
524
519
  ) : effectiveSection === 'habits' ? (
@@ -76,7 +76,8 @@ export function L2ProjectModelDefaultSettings({
76
76
  description: model.provider
77
77
  })
78
78
  )
79
- const formDisabled = state.projectLoading || state.projectRefreshing || state.saving
79
+ const formDisabled = !state.projectReady || state.saving
80
+ const displayedCwd = state.loadedCwd ?? state.cwd
80
81
 
81
82
  async function selectProject(cwd: string): Promise<void> {
82
83
  if (await state.selectProject(cwd)) setMobileDetail(true)
@@ -198,17 +199,19 @@ export function L2ProjectModelDefaultSettings({
198
199
  <div className="pi-desk-chat-scrollbar min-h-0 flex-1 overflow-y-auto p-4 @[48rem]/project-default:p-6">
199
200
  <div className="max-w-[40rem] space-y-6">
200
201
  <header className="space-y-1">
201
- <h3 className="break-all text-lg font-semibold">{projectName(state.cwd)}</h3>
202
- <p className="break-all text-sm text-muted-foreground">{state.cwd}</p>
203
- {state.projectLoading && !state.projectReady ? (
204
- <p
205
- role="status"
206
- className="flex items-center gap-2 text-xs text-muted-foreground"
207
- >
208
- <LoaderCircleIcon aria-hidden="true" className="size-3.5 animate-spin" />
209
- {t('projectLoading')}
210
- </p>
211
- ) : null}
202
+ <h3 className="flex items-center gap-2 text-lg font-semibold">
203
+ <span className="min-w-0 break-all">{projectName(displayedCwd)}</span>
204
+ <span className="inline-flex size-4 shrink-0">
205
+ {!state.projectReady && !state.projectError && (
206
+ <LoaderCircleIcon
207
+ role="status"
208
+ aria-label={t('projectLoading')}
209
+ className="size-4 animate-spin text-muted-foreground"
210
+ />
211
+ )}
212
+ </span>
213
+ </h3>
214
+ <p className="break-all text-sm text-muted-foreground">{displayedCwd}</p>
212
215
  </header>
213
216
 
214
217
  {state.projectError ? (
@@ -221,11 +224,13 @@ export function L2ProjectModelDefaultSettings({
221
224
  variant="outline"
222
225
  size="sm"
223
226
  onClick={state.retryProject}
227
+ disabled={state.projectLoading || state.projectRefreshing}
224
228
  >
225
229
  {t('retry', { ns: 'common' })}
226
230
  </Button>
227
231
  </div>
228
- ) : !state.projectReady ? null : (
232
+ ) : null}
233
+ {state.loadedCwd !== null ? (
229
234
  <fieldset disabled={formDisabled} className="min-w-0 space-y-6">
230
235
  <div className="space-y-1">
231
236
  <p className="text-sm font-medium">{t('projectDefaultSource')}</p>
@@ -289,6 +294,7 @@ export function L2ProjectModelDefaultSettings({
289
294
  type="button"
290
295
  variant="outline"
291
296
  size="xs"
297
+ disabled={formDisabled}
292
298
  className="h-auto min-h-7 max-w-full whitespace-normal text-left"
293
299
  onClick={(): void => state.applyPreset(preset)}
294
300
  >
@@ -309,7 +315,7 @@ export function L2ProjectModelDefaultSettings({
309
315
  </div>
310
316
  ) : null}
311
317
  </fieldset>
312
- )}
318
+ ) : null}
313
319
  </div>
314
320
  </div>
315
321
  <div className="shrink-0 border-t px-4 py-3 @[48rem]/project-default:px-6">
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useSyncExternalStore, type ReactNode } from 'react'
3
+ import { useCallback, useEffect, useSyncExternalStore, type ReactNode } from 'react'
4
4
  import { useTranslation } from 'react-i18next'
5
5
  import { LoaderCircleIcon, TerminalIcon } from 'lucide-react'
6
6
  import { preloadL4TerminalRenderer } from '@client/l4_foundation/terminal/l4-terminal-renderer'
@@ -51,12 +51,46 @@ export function L2TerminalWorkspace({
51
51
  } = useL2TerminalLayout(biz, mobile, expanded)
52
52
  const selected = state.terminals.find((item) => item.terminalId === state.selectedId)
53
53
  const chatHidden = expanded && (mobile || maximized)
54
+ const handleSystemBack = useCallback(
55
+ (event: KeyboardEvent): void => {
56
+ // Android 通过合成 Escape 请求返回;真实键盘 Escape 仍交给 Shell。
57
+ if (
58
+ !mobile ||
59
+ !expanded ||
60
+ event.isTrusted ||
61
+ event.key !== 'Escape' ||
62
+ event.defaultPrevented ||
63
+ event.isComposing ||
64
+ event.altKey ||
65
+ event.ctrlKey ||
66
+ event.metaKey ||
67
+ event.shiftKey ||
68
+ root.current?.closest('[inert]')
69
+ )
70
+ return
71
+ event.preventDefault()
72
+ event.stopImmediatePropagation()
73
+ collapse()
74
+ },
75
+ [collapse, expanded, mobile, root]
76
+ )
77
+ useEffect(() => {
78
+ if (!mobile || !expanded) return
79
+ // 失焦时等菜单、弹窗先消费,再收起终端。
80
+ window.addEventListener('keydown', handleSystemBack)
81
+ return () => window.removeEventListener('keydown', handleSystemBack)
82
+ }, [expanded, handleSystemBack, mobile])
54
83
 
55
84
  return (
56
85
  <div
57
86
  ref={root}
58
87
  className={cn('relative flex size-full min-h-0 min-w-0', right ? 'flex-row' : 'flex-col')}
59
88
  data-terminal-dock={mobile ? 'mobile' : preferences.dock}
89
+ onKeyDownCapture={(event) => {
90
+ if (event.target instanceof Node && event.currentTarget.contains(event.target)) {
91
+ handleSystemBack(event.nativeEvent)
92
+ }
93
+ }}
60
94
  >
61
95
  <ResizablePanelGroup
62
96
  orientation={right ? 'horizontal' : 'vertical'}
@@ -36,6 +36,10 @@ export function L2WorkSessionFilePreview({
36
36
  useSyncExternalStore(runtime.subscribe, runtime.getRevision, runtime.getRevision)
37
37
  const workSession = workSessions.find((item) => item.workId === runtime.getExpandedWorkId())
38
38
  if (!workSession) return null
39
+ const closePreview = (): void => {
40
+ if (mobile) runtime.hideWindow(workSession.workId)
41
+ else runtime.collapseExpandedWindow()
42
+ }
39
43
  const switchToChat = (): void => {
40
44
  const input = document.querySelector<HTMLElement>(
41
45
  `[data-work-id="${CSS.escape(workSession.workId)}"] textarea`
@@ -49,7 +53,7 @@ export function L2WorkSessionFilePreview({
49
53
  <L4AppDialogRoot
50
54
  open
51
55
  onOpenChange={(open) => {
52
- if (!open) runtime.collapseExpandedWindow()
56
+ if (!open) closePreview()
53
57
  }}
54
58
  >
55
59
  <L4AppDialogContent
@@ -103,7 +107,7 @@ export function L2WorkSessionFilePreview({
103
107
  onSwitchPane={switchToChat}
104
108
  onOpenTerminal={onOpenTerminal}
105
109
  onHide={() => runtime.hideWindow(workSession.workId)}
106
- onExpand={() => runtime.collapseExpandedWindow()}
110
+ onExpand={closePreview}
107
111
  />
108
112
  </L4AppDialogContent>
109
113
  </L4AppDialogRoot>