@jetcrab/pi-desk 1.1.10 → 1.1.12-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.
- package/docs/plugins.md +1 -1
- package/package.json +2 -2
- package/src/client/l2_biz/plugin-host/l2-plugin-management.tsx +42 -34
- package/src/client/l2_biz/plugin-host/views/l2-plugin-installed-list.tsx +86 -82
- package/src/client/l2_biz/workbench/hooks/l2-workbench-chat-viewport.ts +42 -27
- package/src/client/l2_biz/workbench/l2-workbench.tsx +4 -7
- package/src/common/l3_modules/project-files/l3-project-files-contract.ts +9 -2
- package/src/server/l4_foundation/file/l4-work-session-project-file.ts +5 -4
- package/temp/build/pi-desk/release/.next/BUILD_ID +1 -1
- package/temp/build/pi-desk/release/.next/build-manifest.json +2 -2
- package/temp/build/pi-desk/release/.next/prerender-manifest.json +3 -3
- package/temp/build/pi-desk/release/.next/required-server-files.js +3 -3
- package/temp/build/pi-desk/release/.next/required-server-files.json +3 -3
- package/temp/build/pi-desk/release/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.html +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.rsc +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/health/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-files/reveal/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/page.js +6 -6
- package/temp/build/pi-desk/release/.next/server/app/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/35985.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/74528.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/8584.js +1 -1
- package/temp/build/pi-desk/release/.next/server/middleware-build-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/pages/500.html +1 -1
- package/temp/build/pi-desk/release/.next/server/server-reference-manifest.json +1 -1
- package/temp/build/pi-desk/release/.next/static/chunks/{45992-304a49aaaebd853e.js → 45992-92d8b8b10fcfbb6b.js} +2 -2
- package/temp/build/pi-desk/release/.next/static/chunks/app/{page-9f478658c50efdbe.js → page-92d5bd2854005e6c.js} +4 -4
- package/temp/build/pi-desk/release/.next/static/css/4a5dda7ff3a82c4a.css +1 -0
- package/temp/build/pi-desk/release/.next/trace-build +1 -1
- package/temp/build/pi-desk/release/.next/static/css/8a41765636e09e52.css +0 -1
- /package/temp/build/pi-desk/release/.next/static/{ZYtCj4W63FjXCrTLTDbvO → qFbsHgU3sMdDOeAhU56jN}/_buildManifest.js +0 -0
- /package/temp/build/pi-desk/release/.next/static/{ZYtCj4W63FjXCrTLTDbvO → qFbsHgU3sMdDOeAhU56jN}/_ssgManifest.js +0 -0
package/docs/plugins.md
CHANGED
|
@@ -77,7 +77,7 @@ plugin.addPreviewDirectory(directory, { default: true })
|
|
|
77
77
|
|
|
78
78
|
`directory` 为服务端绝对目录;省略第二个参数只登记,`default: true` 同时设为默认预览目录,后登记的默认生效。原 CWD 始终可选;目录规范化后去重,最多登记 32 个。重复快照不会覆盖用户的手动选择。登记只在当前会话运行态内,插件在 `session_start`、`session_tree` 从自身数据恢复后重新登记;不要在普通查询时重复请求默认选择。
|
|
79
79
|
|
|
80
|
-
调用不等待回执;非法路径或超限由宿主拒绝并记日志,实际目录内容按需读取。旧宿主或独立 Pi 不处理该登记;兼容旧 SDK 时使用 `plugin.addPreviewDirectory?.(...)
|
|
80
|
+
调用不等待回执;非法路径或超限由宿主拒绝并记日志,实际目录内容按需读取。旧宿主或独立 Pi 不处理该登记;兼容旧 SDK 时使用 `plugin.addPreviewDirectory?.(...)`。`files.preview/reveal` 支持会话 CWD 下规范相对路径或服务端绝对文件路径(包括项目外文件);`preview` 的 `imagePaths` 使用相同路径规则。宿主仍校验当前 Source,目录浏览边界不变。依赖项目外文件的插件需要同步更新宿主,旧宿主只接受项目内相对路径。`readImage` 仍只接受会话 CWD 下规范相对路径。
|
|
81
81
|
|
|
82
82
|
## Browser Entry 与懒界面
|
|
83
83
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jetcrab/pi-desk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.12-dev",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"viewerjs": "^1.11.8",
|
|
70
70
|
"ws": "^8.21.3",
|
|
71
71
|
"zod": "^4.4.3",
|
|
72
|
-
"@jetcrab/pi-desk-sdk": "^1.1.
|
|
72
|
+
"@jetcrab/pi-desk-sdk": "^1.1.3-dev"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@earendil-works/pi-agent-core": "1.2.0",
|
|
@@ -333,41 +333,43 @@ export function L2PluginManagement({
|
|
|
333
333
|
<TabsContent
|
|
334
334
|
value="installed"
|
|
335
335
|
keepMounted
|
|
336
|
-
className="
|
|
336
|
+
className="flex min-h-0 flex-col overflow-hidden p-4 data-[hidden]:hidden sm:p-6"
|
|
337
337
|
>
|
|
338
|
-
<div className="
|
|
338
|
+
<div className="flex min-h-0 flex-1 flex-col gap-4">
|
|
339
339
|
{direct.open ? (
|
|
340
|
-
<
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
management.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
340
|
+
<div className="pi-desk-chat-scrollbar max-h-1/2 shrink-0 overflow-y-auto">
|
|
341
|
+
<L2PluginDirectInstall
|
|
342
|
+
source={direct.source}
|
|
343
|
+
isNpm={Boolean(direct.spec)}
|
|
344
|
+
multiple={direct.sources.length > 1}
|
|
345
|
+
tag={direct.tag}
|
|
346
|
+
selectedVersion={direct.selectedVersion}
|
|
347
|
+
onTag={direct.setTag}
|
|
348
|
+
detail={direct.detail}
|
|
349
|
+
loading={direct.loading}
|
|
350
|
+
pending={directPending}
|
|
351
|
+
disabled={disabled}
|
|
352
|
+
error={direct.error ?? (directErrors || null)}
|
|
353
|
+
operationMessage={
|
|
354
|
+
directPending
|
|
355
|
+
? (operationMessage(directPlugin?.operation?.phase) ?? t('submitting'))
|
|
356
|
+
: null
|
|
357
|
+
}
|
|
358
|
+
override={direct.override}
|
|
359
|
+
registry={direct.registry}
|
|
360
|
+
onSource={direct.changeSource}
|
|
361
|
+
onMode={direct.setOverride}
|
|
362
|
+
onRegistry={direct.setRegistry}
|
|
363
|
+
onRead={() => void direct.loadDetail()}
|
|
364
|
+
onVersion={(version) => void direct.loadDetail(version)}
|
|
365
|
+
onInstall={() =>
|
|
366
|
+
void direct.install(management.requestInstall, (items) =>
|
|
367
|
+
management.setConfirmation({ kind: 'batch', input: { action: 'add', items } })
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
onClose={() => direct.setOpen(false)}
|
|
371
|
+
/>
|
|
372
|
+
</div>
|
|
371
373
|
) : null}
|
|
372
374
|
<L2PluginInstalledList
|
|
373
375
|
snapshot={snapshot}
|
|
@@ -413,7 +415,13 @@ export function L2PluginManagement({
|
|
|
413
415
|
onRefresh={() => void management.refresh()}
|
|
414
416
|
/>
|
|
415
417
|
{!snapshot?.plugins.length && !direct.open ? (
|
|
416
|
-
<Button
|
|
418
|
+
<Button
|
|
419
|
+
size="sm"
|
|
420
|
+
variant="outline"
|
|
421
|
+
className="shrink-0 self-start"
|
|
422
|
+
disabled={disabled}
|
|
423
|
+
onClick={openDirect}
|
|
424
|
+
>
|
|
417
425
|
{t('directInstall')}
|
|
418
426
|
</Button>
|
|
419
427
|
) : null}
|
|
@@ -89,8 +89,8 @@ export function L2PluginInstalledList(props: InstalledListProps): React.JSX.Elem
|
|
|
89
89
|
label: t(`statusFilter_${value}`)
|
|
90
90
|
}))
|
|
91
91
|
return (
|
|
92
|
-
<section aria-label={t('installed')} className="min-w-0
|
|
93
|
-
<div className="
|
|
92
|
+
<section aria-label={t('installed')} className="flex min-h-0 min-w-0 flex-col gap-3">
|
|
93
|
+
<div className="shrink-0 space-y-3 bg-popover pb-2">
|
|
94
94
|
<div className="flex flex-wrap items-center gap-2">
|
|
95
95
|
<Input
|
|
96
96
|
aria-label={t('filterInstalled')}
|
|
@@ -200,92 +200,96 @@ export function L2PluginInstalledList(props: InstalledListProps): React.JSX.Elem
|
|
|
200
200
|
</p>
|
|
201
201
|
) : null}
|
|
202
202
|
</div>
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
<
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
<div className="
|
|
233
|
-
<div className="flex
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
203
|
+
<div className="pi-desk-chat-scrollbar min-h-0 overflow-y-auto">
|
|
204
|
+
{props.loading && !props.snapshot ? (
|
|
205
|
+
<p role="status" className="py-6 text-sm text-muted-foreground">
|
|
206
|
+
{t('loading')}
|
|
207
|
+
</p>
|
|
208
|
+
) : plugins.length ? (
|
|
209
|
+
<div className="divide-y rounded-lg border bg-card">
|
|
210
|
+
{plugins.map((plugin) => {
|
|
211
|
+
const name = pluginDisplayName(plugin)
|
|
212
|
+
const key = pluginRequestKey(plugin.source)
|
|
213
|
+
const browserError = plugin.pluginName
|
|
214
|
+
? (props.browserErrors.get(plugin.pluginName) ?? null)
|
|
215
|
+
: null
|
|
216
|
+
return (
|
|
217
|
+
<article
|
|
218
|
+
key={key}
|
|
219
|
+
aria-label={name}
|
|
220
|
+
className={`min-w-0 px-4 py-3 ${props.selected.has(key) && props.batchMode ? 'bg-accent/40' : ''}`}
|
|
221
|
+
>
|
|
222
|
+
<div className="flex items-start gap-3">
|
|
223
|
+
{props.batchMode && plugin.kind !== 'extension' ? (
|
|
224
|
+
<div className="flex min-h-8 shrink-0 items-center">
|
|
225
|
+
<Checkbox
|
|
226
|
+
checked={props.selected.has(key)}
|
|
227
|
+
aria-label={t('selectPlugin', { name })}
|
|
228
|
+
onCheckedChange={(checked) => props.onSelect([key], checked)}
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
) : null}
|
|
232
|
+
<div className="min-w-0 flex-1 space-y-1">
|
|
233
|
+
<div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
|
|
234
|
+
<div className="flex min-w-0 flex-1 basis-64 flex-wrap items-center gap-x-2 gap-y-1">
|
|
235
|
+
<Button
|
|
236
|
+
variant="link"
|
|
237
|
+
className="h-auto min-h-8 min-w-0 max-w-full justify-start p-0 text-left text-sm font-semibold whitespace-normal no-underline hover:underline [overflow-wrap:anywhere]"
|
|
238
|
+
aria-label={t('viewDetails', { name })}
|
|
239
|
+
onClick={() => props.onDetail(plugin.source)}
|
|
240
|
+
>
|
|
241
|
+
{name}
|
|
242
|
+
</Button>
|
|
243
|
+
{plugin.version ? (
|
|
244
|
+
<span className="text-xs text-muted-foreground">v{plugin.version}</span>
|
|
245
|
+
) : null}
|
|
246
|
+
{plugin.updateAvailable ? (
|
|
247
|
+
<span className="inline-flex rounded-md bg-status-success/10 px-2 py-0.5 text-xs font-medium text-status-success">
|
|
248
|
+
{plugin.availableVersion
|
|
249
|
+
? t('newVersion', { version: plugin.availableVersion })
|
|
250
|
+
: t('updateAvailable')}
|
|
251
|
+
</span>
|
|
252
|
+
) : null}
|
|
253
|
+
{plugin.updateTag && plugin.updateTag !== 'latest' ? (
|
|
254
|
+
<span className="text-xs text-muted-foreground">
|
|
255
|
+
{plugin.updateTag === 'dev' ? t('channelDev') : plugin.updateTag}
|
|
256
|
+
</span>
|
|
257
|
+
) : null}
|
|
258
|
+
</div>
|
|
259
|
+
<L2PluginManagementControls
|
|
260
|
+
plugin={plugin}
|
|
261
|
+
displayName={name}
|
|
262
|
+
browserError={browserError}
|
|
263
|
+
disabled={props.disabled}
|
|
264
|
+
pending={busy(plugin)}
|
|
265
|
+
browserRetryPending={props.requests.has(`entry:${plugin.pluginName}`)}
|
|
266
|
+
onAction={(action) => props.onAction(plugin, action)}
|
|
267
|
+
/>
|
|
257
268
|
</div>
|
|
258
|
-
|
|
269
|
+
{plugin.description ? (
|
|
270
|
+
<p className="line-clamp-2 text-sm text-muted-foreground [overflow-wrap:anywhere]">
|
|
271
|
+
{selectL4LocalizedText(plugin.description, locale)}
|
|
272
|
+
</p>
|
|
273
|
+
) : null}
|
|
274
|
+
<L2PluginManagementFeedback
|
|
259
275
|
plugin={plugin}
|
|
260
|
-
displayName={name}
|
|
261
276
|
browserError={browserError}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
onAction={(action) => props.onAction(plugin, action)}
|
|
277
|
+
requestError={
|
|
278
|
+
props.errors[key] ?? props.errors[`entry:${plugin.pluginName}`]
|
|
279
|
+
}
|
|
266
280
|
/>
|
|
267
281
|
</div>
|
|
268
|
-
{plugin.description ? (
|
|
269
|
-
<p className="line-clamp-2 text-sm text-muted-foreground [overflow-wrap:anywhere]">
|
|
270
|
-
{selectL4LocalizedText(plugin.description, locale)}
|
|
271
|
-
</p>
|
|
272
|
-
) : null}
|
|
273
|
-
<L2PluginManagementFeedback
|
|
274
|
-
plugin={plugin}
|
|
275
|
-
browserError={browserError}
|
|
276
|
-
requestError={props.errors[key] ?? props.errors[`entry:${plugin.pluginName}`]}
|
|
277
|
-
/>
|
|
278
282
|
</div>
|
|
279
|
-
</
|
|
280
|
-
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
283
|
+
</article>
|
|
284
|
+
)
|
|
285
|
+
})}
|
|
286
|
+
</div>
|
|
287
|
+
) : (
|
|
288
|
+
<p className="py-8 text-center text-sm text-muted-foreground">
|
|
289
|
+
{t(props.filter || props.statusFilter !== 'all' ? 'noMatches' : 'empty')}
|
|
290
|
+
</p>
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
289
293
|
</section>
|
|
290
294
|
)
|
|
291
295
|
}
|
|
@@ -83,6 +83,11 @@ export function L2WorkbenchChatViewportManager({
|
|
|
83
83
|
const positioningRef = useRef(false)
|
|
84
84
|
const initializedRef = useRef(false)
|
|
85
85
|
const positionedUserAnchorRef = useRef<string | null>(null)
|
|
86
|
+
// 首屏定位目标跨布局效果重建保留,开始交互或离开视图时释放。
|
|
87
|
+
const entryPositionRef = useRef<{
|
|
88
|
+
anchor: L2WorkbenchChatViewportSnapshot | null
|
|
89
|
+
follow: boolean
|
|
90
|
+
} | null>(null)
|
|
86
91
|
// 只在首次进入仍在运行的视图时恢复旧锚点;之后的新用户消息使用自己的锚点。
|
|
87
92
|
const entrySnapshotRef = useRef(running ? entrySnapshot : null)
|
|
88
93
|
const suspendedViewportRef = useRef<
|
|
@@ -109,12 +114,14 @@ export function L2WorkbenchChatViewportManager({
|
|
|
109
114
|
if (!active && !running) {
|
|
110
115
|
initializedRef.current = false
|
|
111
116
|
entrySnapshotRef.current = null
|
|
117
|
+
entryPositionRef.current = null
|
|
112
118
|
suspendedViewportRef.current = null
|
|
113
119
|
stopScroll()
|
|
114
120
|
logL3ConversationScroll('viewport:leave-ended', scrollElement)
|
|
115
121
|
return
|
|
116
122
|
}
|
|
117
123
|
if (!visible) {
|
|
124
|
+
entryPositionRef.current = null
|
|
118
125
|
if (initializedRef.current && suspendedViewportRef.current === null) {
|
|
119
126
|
// stopScroll 会清掉库的跟随状态;查看文件仍只暂停阅读,不恢复跟随。
|
|
120
127
|
suspendedViewportRef.current =
|
|
@@ -171,6 +178,7 @@ export function L2WorkbenchChatViewportManager({
|
|
|
171
178
|
if (returnedToPage && visible && !running) {
|
|
172
179
|
initializedRef.current = false
|
|
173
180
|
entrySnapshotRef.current = null
|
|
181
|
+
entryPositionRef.current = null
|
|
174
182
|
}
|
|
175
183
|
if (!ready || !visible || !pageVisible) return
|
|
176
184
|
if (!running) onClearSnapshot()
|
|
@@ -178,7 +186,8 @@ export function L2WorkbenchChatViewportManager({
|
|
|
178
186
|
const initializing = !initializedRef.current
|
|
179
187
|
const hasNewUser =
|
|
180
188
|
latestUserAnchor !== null && latestUserAnchor !== positionedUserAnchorRef.current
|
|
181
|
-
|
|
189
|
+
const entryPosition = hasNewUser ? null : entryPositionRef.current
|
|
190
|
+
if (!initializing && !hasNewUser && !entryPosition) return
|
|
182
191
|
if (initializing && !running && !containsTail) {
|
|
183
192
|
onResetToTail()
|
|
184
193
|
return
|
|
@@ -186,6 +195,7 @@ export function L2WorkbenchChatViewportManager({
|
|
|
186
195
|
|
|
187
196
|
const currentScrollElement = scrollRef.current
|
|
188
197
|
if (hasNewUser) {
|
|
198
|
+
entryPositionRef.current = null
|
|
189
199
|
onClearSnapshot()
|
|
190
200
|
if (currentScrollElement) clearL3ConversationTailSpacer(currentScrollElement)
|
|
191
201
|
}
|
|
@@ -211,7 +221,11 @@ export function L2WorkbenchChatViewportManager({
|
|
|
211
221
|
|
|
212
222
|
let target: HTMLElement | null = null
|
|
213
223
|
let targetOffset = L3_CONVERSATION_TOP_GUTTER_PX
|
|
214
|
-
const snapshot =
|
|
224
|
+
const snapshot = entryPosition
|
|
225
|
+
? entryPosition.anchor
|
|
226
|
+
: initializing && running
|
|
227
|
+
? entrySnapshotRef.current
|
|
228
|
+
: null
|
|
215
229
|
if (snapshot) {
|
|
216
230
|
const savedTarget = findL3ConversationMessage(
|
|
217
231
|
scrollElement,
|
|
@@ -232,30 +246,31 @@ export function L2WorkbenchChatViewportManager({
|
|
|
232
246
|
}
|
|
233
247
|
target ??= findL3ConversationMessage(scrollElement, 'data-chat-user-message')
|
|
234
248
|
|
|
235
|
-
const followAfterPosition =
|
|
236
|
-
|
|
237
|
-
|
|
249
|
+
const followAfterPosition =
|
|
250
|
+
entryPosition?.follow ??
|
|
251
|
+
(target !== null && snapshot === null && (running || !initializing))
|
|
252
|
+
const enterRunningAtBottom =
|
|
253
|
+
entryPosition?.follow ?? (initializing && running && snapshot === null)
|
|
254
|
+
const entryTarget = target
|
|
255
|
+
const positionEntry = (): void => {
|
|
238
256
|
stopScroll()
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
257
|
+
if (entryTarget) {
|
|
258
|
+
positionL3ConversationMessage(scrollElement, entryTarget, targetOffset, containsTail)
|
|
259
|
+
} else {
|
|
260
|
+
clearL3ConversationTailSpacer(scrollElement)
|
|
261
|
+
}
|
|
262
|
+
if (enterRunningAtBottom || !entryTarget) {
|
|
263
|
+
// 首屏懒布局展开后也在绘制前置底,不交给后续缓动收敛。
|
|
242
264
|
scrollElement.scrollTop = Math.max(
|
|
243
265
|
0,
|
|
244
266
|
scrollElement.scrollHeight - scrollElement.clientHeight
|
|
245
267
|
)
|
|
246
268
|
}
|
|
247
269
|
markProgrammaticScroll(state, scrollElement)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
scrollElement.scrollTop = Math.max(
|
|
251
|
-
0,
|
|
252
|
-
scrollElement.scrollHeight - scrollElement.clientHeight
|
|
253
|
-
)
|
|
254
|
-
void scrollToBottom({ animation: 'instant' })
|
|
270
|
+
// 同步确立跟随,立即切走也不能丢失这个状态。
|
|
271
|
+
if (followAfterPosition || !entryTarget) void scrollToBottom({ animation: 'instant' })
|
|
255
272
|
}
|
|
256
|
-
|
|
257
|
-
// 定位时就确立跟随,不能等下一帧,否则立即切走会丢失这个状态。
|
|
258
|
-
if (followAfterPosition) void scrollToBottom({ animation: 'instant' })
|
|
273
|
+
positionEntry()
|
|
259
274
|
|
|
260
275
|
logL3ConversationScroll('viewport:initialize:positioned', scrollElement, {
|
|
261
276
|
initializing,
|
|
@@ -267,21 +282,21 @@ export function L2WorkbenchChatViewportManager({
|
|
|
267
282
|
initializedRef.current = true
|
|
268
283
|
positionedUserAnchorRef.current = latestUserAnchor
|
|
269
284
|
entrySnapshotRef.current = null
|
|
270
|
-
if (initializing
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
})
|
|
285
|
+
if (initializing || entryPosition) {
|
|
286
|
+
const anchor = target?.dataset.chatScrollAnchor
|
|
287
|
+
entryPositionRef.current = {
|
|
288
|
+
anchor: anchor ? { messageId: anchor, offset: targetOffset } : null,
|
|
289
|
+
follow: followAfterPosition || target === null
|
|
290
|
+
}
|
|
291
|
+
const observer = new ResizeObserver(positionEntry)
|
|
278
292
|
observer.observe(scrollElement)
|
|
279
293
|
if (scrollElement.firstElementChild) observer.observe(scrollElement.firstElementChild)
|
|
280
294
|
const interactionElement = scrollElement.parentElement!
|
|
281
295
|
const events = ['wheel', 'touchstart', 'pointerdown', 'keydown', 'click'] as const
|
|
282
|
-
const release = (): void => {
|
|
296
|
+
const release = (event?: Event): void => {
|
|
283
297
|
observer.disconnect()
|
|
284
298
|
for (const event of events) interactionElement.removeEventListener(event, release, true)
|
|
299
|
+
if (event) entryPositionRef.current = null
|
|
285
300
|
releaseEntryAnchor = null
|
|
286
301
|
positioningRef.current = false
|
|
287
302
|
}
|
|
@@ -25,10 +25,7 @@ import {
|
|
|
25
25
|
type L2PiDirectoryListResponse
|
|
26
26
|
} from '@common/l2_biz/pi-session/l2-pi-session-contract'
|
|
27
27
|
import type { L2PluginManagementSnapshot } from '@common/l2_biz/plugin/l2-plugin-management-contract'
|
|
28
|
-
import {
|
|
29
|
-
L2WorkSessionFilePathSchema,
|
|
30
|
-
L2WorkSessionFilePreviewPathSchema
|
|
31
|
-
} from '@common/l2_biz/work-session/l2-work-session-file-contract'
|
|
28
|
+
import { L2WorkSessionFilePreviewPathSchema } from '@common/l2_biz/work-session/l2-work-session-file-contract'
|
|
32
29
|
import type {
|
|
33
30
|
L2WorkSessionListItem,
|
|
34
31
|
L2WorkSessionListResponse
|
|
@@ -1614,8 +1611,8 @@ export function L2Workbench({
|
|
|
1614
1611
|
mode: BrowserFilePreviewMode,
|
|
1615
1612
|
imagePathsInput?: readonly string[]
|
|
1616
1613
|
): void => {
|
|
1617
|
-
const path =
|
|
1618
|
-
const images =
|
|
1614
|
+
const path = L2WorkSessionFilePreviewPathSchema.safeParse(pathInput)
|
|
1615
|
+
const images = L2WorkSessionFilePreviewPathSchema.array()
|
|
1619
1616
|
.nonempty()
|
|
1620
1617
|
.optional()
|
|
1621
1618
|
.safeParse(imagePathsInput)
|
|
@@ -1663,7 +1660,7 @@ export function L2Workbench({
|
|
|
1663
1660
|
)
|
|
1664
1661
|
const revealWorkSessionFile = useCallback(
|
|
1665
1662
|
async (source: PluginSource, pathInput: string): Promise<void> => {
|
|
1666
|
-
const path =
|
|
1663
|
+
const path = L2WorkSessionFilePreviewPathSchema.safeParse(pathInput)
|
|
1667
1664
|
if (!path.success) throw new Error(t('invalidFilePath'))
|
|
1668
1665
|
const current = workSessionsRef.current.find(
|
|
1669
1666
|
(item) =>
|
|
@@ -47,9 +47,16 @@ export type L3ProjectRevealTargetType = z.infer<typeof L3ProjectRevealTargetType
|
|
|
47
47
|
export const L3ProjectFileReveal = {
|
|
48
48
|
path: '/api/project-files/reveal',
|
|
49
49
|
request: L3ProjectReadContextSchema.extend({
|
|
50
|
-
path: L4ProjectDirectoryPathSchema,
|
|
50
|
+
path: z.union([L4ProjectDirectoryPathSchema, L4ProjectFilePreviewPathSchema]),
|
|
51
51
|
targetType: L3ProjectRevealTargetTypeSchema.optional()
|
|
52
|
-
})
|
|
52
|
+
})
|
|
53
|
+
.refine((input) => input.path !== '' || input.targetType === 'directory', '文件路径不能为空')
|
|
54
|
+
.refine(
|
|
55
|
+
(input) =>
|
|
56
|
+
(input.workId && input.targetType !== 'directory') ||
|
|
57
|
+
L4ProjectDirectoryPathSchema.safeParse(input.path).success,
|
|
58
|
+
'独立项目和目录定位只接受根内相对路径'
|
|
59
|
+
),
|
|
53
60
|
response: z.object({}).strict()
|
|
54
61
|
} as const
|
|
55
62
|
|
|
@@ -159,8 +159,9 @@ async function resolveProjectFileRevealTarget(
|
|
|
159
159
|
root: string,
|
|
160
160
|
protocolPath: string
|
|
161
161
|
): Promise<ProjectFileRevealTarget> {
|
|
162
|
-
const
|
|
163
|
-
|
|
162
|
+
const absolute = isAbsolute(protocolPath)
|
|
163
|
+
const unresolved = resolve(root, protocolPath)
|
|
164
|
+
if (!absolute && !isInsideRoot(root, unresolved)) {
|
|
164
165
|
throw new L4WorkSessionProjectFileError(
|
|
165
166
|
'outside-project',
|
|
166
167
|
`Project path escapes root: ${protocolPath}`
|
|
@@ -176,7 +177,7 @@ async function resolveProjectFileRevealTarget(
|
|
|
176
177
|
`Containing directory was not found: ${protocolPath}`
|
|
177
178
|
)
|
|
178
179
|
}
|
|
179
|
-
if (!isInsideRoot(root, directory)) {
|
|
180
|
+
if (!absolute && !isInsideRoot(root, directory)) {
|
|
180
181
|
throw new L4WorkSessionProjectFileError(
|
|
181
182
|
'outside-project',
|
|
182
183
|
`Containing directory resolves outside root: ${protocolPath}`
|
|
@@ -198,7 +199,7 @@ async function resolveProjectFileRevealTarget(
|
|
|
198
199
|
}
|
|
199
200
|
throw new L4WorkSessionProjectFileError('read-failed', `无法读取定位目标:${protocolPath}`)
|
|
200
201
|
}
|
|
201
|
-
if (!isInsideRoot(root, target)) {
|
|
202
|
+
if (!absolute && !isInsideRoot(root, target)) {
|
|
202
203
|
throw new L4WorkSessionProjectFileError(
|
|
203
204
|
'outside-project',
|
|
204
205
|
`Project path resolves outside root: ${protocolPath}`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
qFbsHgU3sMdDOeAhU56jN
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
],
|
|
5
5
|
"devFiles": [],
|
|
6
6
|
"lowPriorityFiles": [
|
|
7
|
-
"static/
|
|
8
|
-
"static/
|
|
7
|
+
"static/qFbsHgU3sMdDOeAhU56jN/_buildManifest.js",
|
|
8
|
+
"static/qFbsHgU3sMdDOeAhU56jN/_ssgManifest.js"
|
|
9
9
|
],
|
|
10
10
|
"rootMainFiles": [
|
|
11
11
|
"static/chunks/webpack-735430d61da52e9d.js",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"dynamicRoutes": {},
|
|
66
66
|
"notFoundRoutes": [],
|
|
67
67
|
"preview": {
|
|
68
|
-
"previewModeId": "
|
|
69
|
-
"previewModeSigningKey": "
|
|
70
|
-
"previewModeEncryptionKey": "
|
|
68
|
+
"previewModeId": "6a3ed0add2f10c8e629c3b4927c3ab47",
|
|
69
|
+
"previewModeSigningKey": "41ebb995a07e123fb88ed9cf02a7c140ce93d47834ee0cb7d9c6c9dcc462cca1",
|
|
70
|
+
"previewModeEncryptionKey": "7ba998796c3a2cb1cef990841e56a970e006ce0833a171001b0266ed108d6aaa"
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -2,12 +2,12 @@ self.__SERVER_FILES_MANIFEST={
|
|
|
2
2
|
"version": 1,
|
|
3
3
|
"config": {
|
|
4
4
|
"env": {
|
|
5
|
-
"PI_DESK_APP_VERSION": "1.1.
|
|
6
|
-
"PI_DESK_VERSION_UPDATED_AT": "2026-10-
|
|
5
|
+
"PI_DESK_APP_VERSION": "1.1.12-dev",
|
|
6
|
+
"PI_DESK_VERSION_UPDATED_AT": "2026-10-14T09:06:23+08:00"
|
|
7
7
|
},
|
|
8
8
|
"webpack": null,
|
|
9
9
|
"typescript": {
|
|
10
|
-
"ignoreBuildErrors":
|
|
10
|
+
"ignoreBuildErrors": true
|
|
11
11
|
},
|
|
12
12
|
"typedRoutes": false,
|
|
13
13
|
"distDir": "temp/build/pi-desk/release/.next",
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"version": 1,
|
|
3
3
|
"config": {
|
|
4
4
|
"env": {
|
|
5
|
-
"PI_DESK_APP_VERSION": "1.1.
|
|
6
|
-
"PI_DESK_VERSION_UPDATED_AT": "2026-10-
|
|
5
|
+
"PI_DESK_APP_VERSION": "1.1.12-dev",
|
|
6
|
+
"PI_DESK_VERSION_UPDATED_AT": "2026-10-14T09:06:23+08:00"
|
|
7
7
|
},
|
|
8
8
|
"webpack": null,
|
|
9
9
|
"typescript": {
|
|
10
|
-
"ignoreBuildErrors":
|
|
10
|
+
"ignoreBuildErrors": true
|
|
11
11
|
},
|
|
12
12
|
"typedRoutes": false,
|
|
13
13
|
"distDir": "temp/build/pi-desk/release/.next",
|