@jetcrab/pi-desk 1.1.8 → 1.1.10-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 +15 -1
- package/package.json +7 -7
- package/src/client/l2_biz/auth/l2-login-biz.ts +4 -1
- package/src/client/l2_biz/settings/hooks/l2-use-auth-settings.ts +18 -1
- package/src/client/l2_biz/settings/l2-auth-settings-biz.ts +18 -1
- package/src/client/l2_biz/settings/l2-auth-settings.tsx +19 -5
- package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-runtime.ts +144 -23
- package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-view.tsx +9 -5
- package/src/client/l2_biz/workbench/file-workspace/l2-project-file-browser.tsx +94 -5
- package/src/client/l2_biz/workbench/file-workspace/l2-project-preview-biz.ts +48 -0
- package/src/client/l2_biz/workbench/git-workspace/l2-git-workspace-runtime.ts +26 -8
- package/src/client/l2_biz/workbench/hooks/l2-workbench-chat-viewport.ts +70 -47
- package/src/client/l2_biz/workbench/l2-work-session-columns.tsx +1 -0
- package/src/client/l2_biz/workbench/l2-workbench-chat-view.tsx +18 -14
- package/src/client/l2_biz/workbench/l2-workbench.tsx +37 -6
- package/src/client/l3_modules/conversation/l3-conversation-messages.tsx +28 -9
- package/src/client/l3_modules/project-files/l3-project-file-browser.tsx +27 -15
- package/src/client/l3_modules/project-files/l3-project-file-runtime.ts +7 -0
- package/src/client/l4_foundation/ui/code/l4-code-preferences.ts +1 -1
- package/src/common/l2_biz/auth/l2-auth-contract.ts +14 -1
- package/src/common/l2_biz/auth/l2-auth-messages.ts +27 -10
- package/src/common/l3_modules/app-runtime/l3-app-runtime-contract.ts +4 -1
- package/src/common/l3_modules/conversation/l3-conversation-locale-messages.ts +4 -0
- package/src/common/l3_modules/project-files/l3-project-files-locale-messages.ts +8 -0
- package/src/common/l4_foundation/file/l4-project-preview-contract.ts +25 -0
- package/src/server/l1_entry/api/l1-auth-login-route.ts +20 -6
- package/src/server/l1_entry/api/l1-auth-settings-routes.ts +1 -1
- package/src/server/l1_entry/api/l1-project-route.ts +14 -5
- package/src/server/l1_entry/node/l1-server.ts +4 -7
- package/src/server/l2_biz/app-runtime/l2-app-runtime.ts +6 -1
- package/src/server/l2_biz/auth/l2-auth.ts +52 -2
- package/src/server/l2_biz/work-session/l2-work-session-chat-runtime.ts +28 -0
- package/src/server/l2_biz/work-session/l2-work-session-manage.ts +8 -0
- package/src/server/l3_modules/app-runtime/l3-app-notification-runtime.ts +48 -14
- package/src/server/l4_foundation/auth/l4-auth-config.ts +19 -3
- package/src/server/l4_foundation/pi/l4-pi-chat-worker.ts +2 -1
- package/src/server/l4_foundation/pi/l4-pi-global-runtime.js +3 -13
- package/src/server/l4_foundation/pi/l4-pi-session-plugin-runtime.ts +60 -0
- package/temp/build/pi-desk/release/.next/BUILD_ID +1 -1
- package/temp/build/pi-desk/release/.next/build-manifest.json +3 -3
- package/temp/build/pi-desk/release/.next/prerender-manifest.json +3 -3
- package/temp/build/pi-desk/release/.next/react-loadable-manifest.json +1 -1
- 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/auth/login/route.js +2 -4
- package/temp/build/pi-desk/release/.next/server/app/api/auth/logout/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/auth-settings/get/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/auth-settings/replace/route.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.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 +64 -64
- 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/23054.js +2 -2
- package/temp/build/pi-desk/release/.next/server/chunks/25406.js +7 -7
- package/temp/build/pi-desk/release/.next/server/chunks/35985.js +3 -3
- package/temp/build/pi-desk/release/.next/server/chunks/49900.js +4 -0
- package/temp/build/pi-desk/release/.next/server/chunks/5034.js +1 -0
- package/temp/build/pi-desk/release/.next/server/chunks/50520.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/61211.js +2 -0
- package/temp/build/pi-desk/release/.next/server/chunks/6917.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/77953.js +1 -0
- package/temp/build/pi-desk/release/.next/server/chunks/8584.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/92386.js +4 -4
- package/temp/build/pi-desk/release/.next/server/chunks/93049.js +1 -0
- package/temp/build/pi-desk/release/.next/server/chunks/94296.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/98227.js +3 -0
- package/temp/build/pi-desk/release/.next/server/instrumentation.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/middleware-react-loadable-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 +13 -0
- package/temp/build/pi-desk/release/.next/static/chunks/{34903-e4205c0becabea76.js → 47340-c1c09c77c1a88514.js} +1 -1
- package/temp/build/pi-desk/release/.next/static/chunks/70669.07f3670bfb7d6dd2.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/{layout-586f6f88483051a7.js → layout-839162b6cfae8347.js} +1 -1
- package/temp/build/pi-desk/release/.next/static/chunks/app/login/page-4532a3bbee48e219.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/page-ca3020d12ae98f64.js +158 -0
- package/temp/build/pi-desk/release/.next/static/chunks/{webpack-26261285e181235a.js → webpack-735430d61da52e9d.js} +1 -1
- package/temp/build/pi-desk/release/.next/static/css/8a41765636e09e52.css +1 -0
- package/temp/build/pi-desk/release/.next/trace-build +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/31442.js +0 -3
- package/temp/build/pi-desk/release/.next/server/chunks/37681.js +0 -1
- package/temp/build/pi-desk/release/.next/server/chunks/65589.js +0 -4
- package/temp/build/pi-desk/release/.next/server/chunks/99672.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/45992-a39a014497ec4dd1.js +0 -13
- package/temp/build/pi-desk/release/.next/static/chunks/70669.8b4231a8675499e0.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/app/login/page-bf893718bb151f3c.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/app/page-c92ecc3b7351294d.js +0 -158
- package/temp/build/pi-desk/release/.next/static/css/3213ee28336bddc2.css +0 -1
- /package/temp/build/pi-desk/release/.next/static/{ZYYO9lnf3gpocPCjcjYmq → d7n5VhA6J-kI3A_-XJ4TQ}/_buildManifest.js +0 -0
- /package/temp/build/pi-desk/release/.next/static/{ZYYO9lnf3gpocPCjcjYmq → d7n5VhA6J-kI3A_-XJ4TQ}/_ssgManifest.js +0 -0
|
@@ -58,6 +58,7 @@ interface MutableCwdGitWorkspace {
|
|
|
58
58
|
interface ProjectRef {
|
|
59
59
|
workId: string
|
|
60
60
|
cwd: string
|
|
61
|
+
readOnly?: boolean
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
function createWorkspace(cwd: string): MutableCwdGitWorkspace {
|
|
@@ -107,7 +108,8 @@ export class L2GitWorkspaceRuntime {
|
|
|
107
108
|
|
|
108
109
|
reconcileWorkSessions(
|
|
109
110
|
workSessions: readonly L2WorkSessionListItem[],
|
|
110
|
-
focusedWorkId: string | null
|
|
111
|
+
focusedWorkId: string | null,
|
|
112
|
+
previewDirectories: readonly Pick<ProjectRef, 'workId' | 'cwd'>[] = []
|
|
111
113
|
): void {
|
|
112
114
|
const next = new Map<string, ProjectRef>()
|
|
113
115
|
const focused = focusedWorkId
|
|
@@ -120,9 +122,16 @@ export class L2GitWorkspaceRuntime {
|
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
for (const directory of previewDirectories) {
|
|
126
|
+
if (!next.has(directory.cwd)) next.set(directory.cwd, { ...directory, readOnly: true })
|
|
127
|
+
}
|
|
123
128
|
const changed =
|
|
124
129
|
next.size !== this.projectRefs.size ||
|
|
125
|
-
[...next].some(
|
|
130
|
+
[...next].some(
|
|
131
|
+
([cwd, ref]) =>
|
|
132
|
+
this.projectRefs.get(cwd)?.workId !== ref.workId ||
|
|
133
|
+
this.projectRefs.get(cwd)?.readOnly !== ref.readOnly
|
|
134
|
+
)
|
|
126
135
|
if (!changed) return
|
|
127
136
|
this.projectRefs.clear()
|
|
128
137
|
for (const [cwd, ref] of next) this.projectRefs.set(cwd, ref)
|
|
@@ -171,7 +180,7 @@ export class L2GitWorkspaceRuntime {
|
|
|
171
180
|
workspace.heads = { ...workspace.heads, status: 'loading', error: null }
|
|
172
181
|
this.publish()
|
|
173
182
|
try {
|
|
174
|
-
const result = await this.biz.getHeads(project, controller.signal)
|
|
183
|
+
const result = await this.biz.getHeads({ workId: project.workId, cwd }, controller.signal)
|
|
175
184
|
if (workspace.headsRequest?.token !== token) return
|
|
176
185
|
workspace.headsRequest = null
|
|
177
186
|
workspace.heads = { status: 'ready', repositories: result.repositories, error: null }
|
|
@@ -238,7 +247,10 @@ export class L2GitWorkspaceRuntime {
|
|
|
238
247
|
this.publish()
|
|
239
248
|
|
|
240
249
|
try {
|
|
241
|
-
const data = await this.biz.listBranches(
|
|
250
|
+
const data = await this.biz.listBranches(
|
|
251
|
+
{ workId: project.workId, cwd, repositoryRoot },
|
|
252
|
+
controller.signal
|
|
253
|
+
)
|
|
242
254
|
if (workspace.branchRequests.get(repositoryRoot)?.token !== token) return
|
|
243
255
|
workspace.branchRequests.delete(repositoryRoot)
|
|
244
256
|
workspace.branches.set(repositoryRoot, { status: 'ready', data, error: null })
|
|
@@ -261,7 +273,7 @@ export class L2GitWorkspaceRuntime {
|
|
|
261
273
|
repositoryRoot: string,
|
|
262
274
|
target: L2WorkSessionGitBranchTarget
|
|
263
275
|
): Promise<void> {
|
|
264
|
-
const project = this.
|
|
276
|
+
const project = this.requireWritableProject(cwd)
|
|
265
277
|
const workspace = this.workspaceFor(cwd)
|
|
266
278
|
if (workspace.branchOperations.has(repositoryRoot)) return
|
|
267
279
|
workspace.branchOperations.add(repositoryRoot)
|
|
@@ -283,7 +295,7 @@ export class L2GitWorkspaceRuntime {
|
|
|
283
295
|
name: string,
|
|
284
296
|
startPoint: L2WorkSessionGitBranchTarget | null
|
|
285
297
|
): Promise<void> {
|
|
286
|
-
const project = this.
|
|
298
|
+
const project = this.requireWritableProject(cwd)
|
|
287
299
|
const workspace = this.workspaceFor(cwd)
|
|
288
300
|
if (workspace.branchOperations.has(repositoryRoot)) return
|
|
289
301
|
workspace.branchOperations.add(repositoryRoot)
|
|
@@ -346,7 +358,7 @@ export class L2GitWorkspaceRuntime {
|
|
|
346
358
|
this.publish()
|
|
347
359
|
|
|
348
360
|
try {
|
|
349
|
-
const result = await this.biz.get(project, controller.signal)
|
|
361
|
+
const result = await this.biz.get({ workId: project.workId, cwd }, controller.signal)
|
|
350
362
|
if (workspace.snapshotRequest?.token !== token) return
|
|
351
363
|
workspace.snapshotRequest = null
|
|
352
364
|
workspace.snapshot = { status: 'ready', repositories: result.repositories, error: null }
|
|
@@ -373,7 +385,13 @@ export class L2GitWorkspaceRuntime {
|
|
|
373
385
|
private requireProject(cwd: string): ProjectRef {
|
|
374
386
|
const project = this.projectRefs.get(cwd)
|
|
375
387
|
if (!project) throw new Error(l2WorkbenchText('gitNoProjectOperation'))
|
|
376
|
-
return project
|
|
388
|
+
return { workId: project.workId, cwd: project.cwd }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private requireWritableProject(cwd: string): ProjectRef {
|
|
392
|
+
if (this.projectRefs.get(cwd)?.readOnly)
|
|
393
|
+
throw new Error(l2WorkbenchText('gitNoProjectOperation'))
|
|
394
|
+
return this.requireProject(cwd)
|
|
377
395
|
}
|
|
378
396
|
|
|
379
397
|
private applyRepository(
|
|
@@ -7,8 +7,7 @@ import {
|
|
|
7
7
|
clearL3ConversationTailSpacer,
|
|
8
8
|
findL3ConversationMessage,
|
|
9
9
|
logL3ConversationScroll,
|
|
10
|
-
positionL3ConversationMessage
|
|
11
|
-
readL3ConversationTailSpacerHeight
|
|
10
|
+
positionL3ConversationMessage
|
|
12
11
|
} from '@client/l3_modules/conversation/l3-conversation-viewport'
|
|
13
12
|
import type { L2WorkbenchChatViewportSnapshot } from '../l2-workbench-chat'
|
|
14
13
|
|
|
@@ -33,6 +32,7 @@ function markProgrammaticScroll(scrollState: StickToBottomState, scrollElement:
|
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
interface L2WorkbenchChatViewportManagerProps {
|
|
35
|
+
active: boolean
|
|
36
36
|
visible?: boolean
|
|
37
37
|
running: boolean
|
|
38
38
|
ready: boolean
|
|
@@ -48,19 +48,7 @@ function activeTailTurn(scrollElement: HTMLElement): HTMLElement | null {
|
|
|
48
48
|
return scrollElement.querySelector<HTMLElement>('[data-chat-tail-space]')
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function captureSnapshot(
|
|
52
|
-
scrollElement: HTMLElement,
|
|
53
|
-
includeBottom = false
|
|
54
|
-
): L2WorkbenchChatViewportSnapshot | null {
|
|
55
|
-
const distanceFromBottom =
|
|
56
|
-
scrollElement.scrollHeight - scrollElement.scrollTop - scrollElement.clientHeight
|
|
57
|
-
if (
|
|
58
|
-
!includeBottom &&
|
|
59
|
-
readL3ConversationTailSpacerHeight(scrollElement) <= BOTTOM_THRESHOLD_PX &&
|
|
60
|
-
distanceFromBottom <= BOTTOM_THRESHOLD_PX
|
|
61
|
-
)
|
|
62
|
-
return null
|
|
63
|
-
|
|
51
|
+
function captureSnapshot(scrollElement: HTMLElement): L2WorkbenchChatViewportSnapshot | null {
|
|
64
52
|
const containerTop = scrollElement.getBoundingClientRect().top
|
|
65
53
|
let closest: { messageId: string; offset: number; distance: number } | null = null
|
|
66
54
|
for (const message of scrollElement.querySelectorAll<HTMLElement>('[data-chat-scroll-anchor]')) {
|
|
@@ -74,6 +62,7 @@ function captureSnapshot(
|
|
|
74
62
|
}
|
|
75
63
|
|
|
76
64
|
export function L2WorkbenchChatViewportManager({
|
|
65
|
+
active,
|
|
77
66
|
visible = true,
|
|
78
67
|
running,
|
|
79
68
|
ready,
|
|
@@ -96,8 +85,9 @@ export function L2WorkbenchChatViewportManager({
|
|
|
96
85
|
const positionedUserAnchorRef = useRef<string | null>(null)
|
|
97
86
|
// 只在首次进入仍在运行的视图时恢复旧锚点;之后的新用户消息使用自己的锚点。
|
|
98
87
|
const entrySnapshotRef = useRef(running ? entrySnapshot : null)
|
|
99
|
-
const
|
|
100
|
-
|
|
88
|
+
const suspendedViewportRef = useRef<
|
|
89
|
+
'follow' | { snapshot: L2WorkbenchChatViewportSnapshot | null; scrollTop: number } | null
|
|
90
|
+
>(null)
|
|
101
91
|
|
|
102
92
|
useLayoutEffect(() => {
|
|
103
93
|
const scrollElement = scrollRef.current
|
|
@@ -115,37 +105,65 @@ export function L2WorkbenchChatViewportManager({
|
|
|
115
105
|
useLayoutEffect(() => {
|
|
116
106
|
const scrollElement = scrollRef.current
|
|
117
107
|
if (!scrollElement) return
|
|
108
|
+
// 切到其他会话后仍按进入规则定位;同会话查看文件只暂停阅读。
|
|
109
|
+
if (!active && !running) {
|
|
110
|
+
initializedRef.current = false
|
|
111
|
+
entrySnapshotRef.current = null
|
|
112
|
+
suspendedViewportRef.current = null
|
|
113
|
+
stopScroll()
|
|
114
|
+
logL3ConversationScroll('viewport:leave-ended', scrollElement)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
118
117
|
if (!visible) {
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
if (initializedRef.current && suspendedViewportRef.current === null) {
|
|
119
|
+
// stopScroll 会清掉库的跟随状态;查看文件仍只暂停阅读,不恢复跟随。
|
|
120
|
+
suspendedViewportRef.current =
|
|
121
|
+
!active && containsTail && state.isAtBottom
|
|
122
|
+
? 'follow'
|
|
123
|
+
: { snapshot: captureSnapshot(scrollElement), scrollTop: scrollElement.scrollTop }
|
|
124
|
+
logL3ConversationScroll('viewport:suspend', scrollElement, {
|
|
125
|
+
follow: suspendedViewportRef.current === 'follow'
|
|
126
|
+
})
|
|
122
127
|
}
|
|
123
128
|
stopScroll()
|
|
124
129
|
return
|
|
125
130
|
}
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
const suspended = suspendedViewportRef.current
|
|
132
|
+
if (suspended === null) return
|
|
133
|
+
if (suspended === 'follow') {
|
|
134
|
+
onClearSnapshot()
|
|
135
|
+
scrollElement.scrollTop = Math.max(0, scrollElement.scrollHeight - scrollElement.clientHeight)
|
|
136
|
+
markProgrammaticScroll(state, scrollElement)
|
|
137
|
+
void scrollToBottom({ animation: 'instant' })
|
|
138
|
+
logL3ConversationScroll('viewport:restore-follow', scrollElement)
|
|
139
|
+
} else {
|
|
140
|
+
stopScroll()
|
|
141
|
+
const { snapshot } = suspended
|
|
142
|
+
const target = snapshot
|
|
143
|
+
? findL3ConversationMessage(scrollElement, 'data-chat-scroll-anchor', snapshot.messageId)
|
|
144
|
+
: null
|
|
145
|
+
if (snapshot && target)
|
|
146
|
+
positionL3ConversationMessage(scrollElement, target, snapshot.offset, containsTail)
|
|
147
|
+
else scrollElement.scrollTop = suspended.scrollTop
|
|
148
|
+
markProgrammaticScroll(state, scrollElement)
|
|
149
|
+
logL3ConversationScroll('viewport:restore-reading', scrollElement, { running, snapshot })
|
|
134
150
|
}
|
|
135
|
-
|
|
136
|
-
const snapshot = suspendedSnapshotRef.current
|
|
137
|
-
const target = snapshot
|
|
138
|
-
? findL3ConversationMessage(scrollElement, 'data-chat-scroll-anchor', snapshot.messageId)
|
|
139
|
-
: null
|
|
140
|
-
if (snapshot && target)
|
|
141
|
-
positionL3ConversationMessage(scrollElement, target, snapshot.offset, containsTail)
|
|
142
|
-
else scrollElement.scrollTop = suspendedScrollTopRef.current
|
|
143
|
-
// 仍在运行时,隐藏期间的新消息不能覆盖离开前的阅读位置。
|
|
151
|
+
// 隐藏期间的新消息或回复结束都不能覆盖离开前的阅读位置。
|
|
144
152
|
initializedRef.current = true
|
|
145
153
|
positionedUserAnchorRef.current = latestUserAnchor
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
154
|
+
suspendedViewportRef.current = null
|
|
155
|
+
}, [
|
|
156
|
+
active,
|
|
157
|
+
containsTail,
|
|
158
|
+
latestUserAnchor,
|
|
159
|
+
onClearSnapshot,
|
|
160
|
+
running,
|
|
161
|
+
scrollRef,
|
|
162
|
+
scrollToBottom,
|
|
163
|
+
state,
|
|
164
|
+
stopScroll,
|
|
165
|
+
visible
|
|
166
|
+
])
|
|
149
167
|
|
|
150
168
|
useLayoutEffect(() => {
|
|
151
169
|
const returnedToPage = pageVisible && !previousPageVisibleRef.current
|
|
@@ -225,8 +243,8 @@ export function L2WorkbenchChatViewportManager({
|
|
|
225
243
|
0,
|
|
226
244
|
scrollElement.scrollHeight - scrollElement.clientHeight
|
|
227
245
|
)
|
|
228
|
-
void scrollToBottom({ animation: 'instant' })
|
|
229
246
|
}
|
|
247
|
+
markProgrammaticScroll(state, scrollElement)
|
|
230
248
|
} else {
|
|
231
249
|
clearL3ConversationTailSpacer(scrollElement)
|
|
232
250
|
scrollElement.scrollTop = Math.max(
|
|
@@ -236,6 +254,9 @@ export function L2WorkbenchChatViewportManager({
|
|
|
236
254
|
void scrollToBottom({ animation: 'instant' })
|
|
237
255
|
}
|
|
238
256
|
|
|
257
|
+
// 定位时就确立跟随,不能等下一帧,否则立即切走会丢失这个状态。
|
|
258
|
+
if (followAfterPosition) void scrollToBottom({ animation: 'instant' })
|
|
259
|
+
|
|
239
260
|
logL3ConversationScroll('viewport:initialize:positioned', scrollElement, {
|
|
240
261
|
initializing,
|
|
241
262
|
running,
|
|
@@ -249,7 +270,6 @@ export function L2WorkbenchChatViewportManager({
|
|
|
249
270
|
if (initializing && !running && target) {
|
|
250
271
|
// 首屏的懒布局仍会改变高度;用户开始操作后立即交还阅读位置。
|
|
251
272
|
const entryTarget = target
|
|
252
|
-
markProgrammaticScroll(state, scrollElement)
|
|
253
273
|
const observer = new ResizeObserver(() => {
|
|
254
274
|
stopScroll()
|
|
255
275
|
positionL3ConversationMessage(scrollElement, entryTarget, targetOffset, containsTail)
|
|
@@ -271,14 +291,12 @@ export function L2WorkbenchChatViewportManager({
|
|
|
271
291
|
releaseEntryAnchor = release
|
|
272
292
|
}
|
|
273
293
|
releaseFrame = window.requestAnimationFrame(() => {
|
|
274
|
-
if (followAfterPosition) void scrollToBottom({ animation: 'instant' })
|
|
275
|
-
else if (target) stopScroll()
|
|
276
294
|
positioningRef.current = releaseEntryAnchor !== null
|
|
277
295
|
})
|
|
278
296
|
}
|
|
279
297
|
|
|
280
|
-
//
|
|
281
|
-
if (
|
|
298
|
+
// 已提交的消息在绘制前定位,避免新消息到达后切走取消待执行的定位。
|
|
299
|
+
if (scrollRef.current) position()
|
|
282
300
|
else positionFrame = window.requestAnimationFrame(position)
|
|
283
301
|
|
|
284
302
|
return () => {
|
|
@@ -338,6 +356,11 @@ export function L2WorkbenchChatViewportManager({
|
|
|
338
356
|
|
|
339
357
|
const save = (): void => {
|
|
340
358
|
if (positioningRef.current) return
|
|
359
|
+
// 留白是消息定位的布局空间,不代表用户退出了跟随。
|
|
360
|
+
if (containsTail && state.isAtBottom) {
|
|
361
|
+
onClearSnapshot()
|
|
362
|
+
return
|
|
363
|
+
}
|
|
341
364
|
const snapshot = captureSnapshot(scrollElement)
|
|
342
365
|
if (snapshot) onSaveSnapshot(snapshot)
|
|
343
366
|
else onClearSnapshot()
|
|
@@ -347,7 +370,7 @@ export function L2WorkbenchChatViewportManager({
|
|
|
347
370
|
scrollElement.removeEventListener('scroll', save)
|
|
348
371
|
save()
|
|
349
372
|
}
|
|
350
|
-
}, [onClearSnapshot, onSaveSnapshot, running, scrollRef, visible])
|
|
373
|
+
}, [containsTail, onClearSnapshot, onSaveSnapshot, running, scrollRef, state, visible])
|
|
351
374
|
|
|
352
375
|
return null
|
|
353
376
|
}
|
|
@@ -25,6 +25,7 @@ import { L2WorkbenchChatNavigation } from './hooks/l2-workbench-chat-navigation'
|
|
|
25
25
|
import { L2WorkbenchChatViewportManager } from './hooks/l2-workbench-chat-viewport'
|
|
26
26
|
|
|
27
27
|
interface L2WorkbenchChatViewProps {
|
|
28
|
+
active: boolean
|
|
28
29
|
visible: boolean
|
|
29
30
|
workSession: L2WorkSessionListItem
|
|
30
31
|
chatRuntime: L2WorkbenchChatRuntime
|
|
@@ -33,6 +34,7 @@ interface L2WorkbenchChatViewProps {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
function L2WorkbenchChatViewComponent({
|
|
37
|
+
active,
|
|
36
38
|
visible,
|
|
37
39
|
workSession,
|
|
38
40
|
chatRuntime,
|
|
@@ -138,22 +140,9 @@ function L2WorkbenchChatViewComponent({
|
|
|
138
140
|
>
|
|
139
141
|
{(turnWindow) => (
|
|
140
142
|
<>
|
|
141
|
-
<L2WorkbenchChatViewportManager
|
|
142
|
-
key={`viewport:${source.sessionId}:${source.branchId}`}
|
|
143
|
-
visible={visible}
|
|
144
|
-
running={viewportRunning}
|
|
145
|
-
ready={hasMessages}
|
|
146
|
-
containsTail={turnWindow.containsTail}
|
|
147
|
-
latestUserAnchor={latestUserAnchor}
|
|
148
|
-
entrySnapshot={entrySnapshot}
|
|
149
|
-
onResetToTail={turnWindow.resetToTail}
|
|
150
|
-
onSaveSnapshot={saveViewportSnapshot}
|
|
151
|
-
onClearSnapshot={clearViewportSnapshot}
|
|
152
|
-
/>
|
|
153
|
-
<L3ConversationTailSpacerObserver active={visible} />
|
|
154
143
|
<L3ConversationStickToBottomBridge>
|
|
155
144
|
<ConversationContent
|
|
156
|
-
scrollClassName="pi-desk-chat-scrollbar pi-desk-chat-conversation-scroll"
|
|
145
|
+
scrollClassName="pi-desk-chat-scrollbar pi-desk-chat-conversation-scroll overflow-auto"
|
|
157
146
|
className="mx-auto min-h-full w-full max-w-3xl gap-6 px-3 pt-3 pb-4 sm:px-5"
|
|
158
147
|
>
|
|
159
148
|
{turnWindow.visibleTurns.map((turn, index) => {
|
|
@@ -172,6 +161,21 @@ function L2WorkbenchChatViewComponent({
|
|
|
172
161
|
})}
|
|
173
162
|
</ConversationContent>
|
|
174
163
|
</L3ConversationStickToBottomBridge>
|
|
164
|
+
{/* 内容先挂载,确保视口管理器的布局 effect 能取得滚动节点。 */}
|
|
165
|
+
<L2WorkbenchChatViewportManager
|
|
166
|
+
key={`viewport:${source.sessionId}:${source.branchId}`}
|
|
167
|
+
active={active}
|
|
168
|
+
visible={visible}
|
|
169
|
+
running={viewportRunning}
|
|
170
|
+
ready={hasMessages}
|
|
171
|
+
containsTail={turnWindow.containsTail}
|
|
172
|
+
latestUserAnchor={latestUserAnchor}
|
|
173
|
+
entrySnapshot={entrySnapshot}
|
|
174
|
+
onResetToTail={turnWindow.resetToTail}
|
|
175
|
+
onSaveSnapshot={saveViewportSnapshot}
|
|
176
|
+
onClearSnapshot={clearViewportSnapshot}
|
|
177
|
+
/>
|
|
178
|
+
<L3ConversationTailSpacerObserver active={visible} />
|
|
175
179
|
<L2WorkbenchChatNavigation
|
|
176
180
|
key={`navigation:${source.sessionId}:${source.branchId}`}
|
|
177
181
|
active={visible}
|
|
@@ -72,6 +72,10 @@ import { createL2WorkbenchBiz } from './l2-workbench-biz'
|
|
|
72
72
|
import { createL2WorkbenchVersionRefresh } from './l2-workbench-version-refresh'
|
|
73
73
|
import { L2FileWorkspaceRuntime } from './file-workspace/l2-file-workspace-runtime'
|
|
74
74
|
import { L2ProjectFileBrowser } from './file-workspace/l2-project-file-browser'
|
|
75
|
+
import {
|
|
76
|
+
subscribeL2ProjectPreviewDirectories,
|
|
77
|
+
syncL2ProjectPreviewGitReferences
|
|
78
|
+
} from './file-workspace/l2-project-preview-biz'
|
|
75
79
|
import {
|
|
76
80
|
L2PluginStandaloneFilePreview,
|
|
77
81
|
type L2PluginStandaloneFilePreviewTarget
|
|
@@ -607,8 +611,8 @@ export function L2Workbench({
|
|
|
607
611
|
|
|
608
612
|
workSessionsInitializedRef.current = true
|
|
609
613
|
pluginHost.replaceWorkSessions(next.workSessions)
|
|
610
|
-
fileRuntime.reconcileWorkSessions(next.workSessions)
|
|
611
614
|
workSessionsRef.current = next.workSessions
|
|
615
|
+
fileRuntime.reconcileWorkSessions(next.workSessions)
|
|
612
616
|
pinnedCountRef.current = next.pinnedCount
|
|
613
617
|
setPluginStandaloneFilePreview((current) => {
|
|
614
618
|
if (!current) return null
|
|
@@ -811,8 +815,20 @@ export function L2Workbench({
|
|
|
811
815
|
}, [])
|
|
812
816
|
|
|
813
817
|
useEffect(() => {
|
|
814
|
-
gitRuntime
|
|
815
|
-
}, [focusedWorkId, gitRuntime, workSessions])
|
|
818
|
+
syncL2ProjectPreviewGitReferences(fileRuntime, gitRuntime, workSessions, focusedWorkId)
|
|
819
|
+
}, [fileRuntime, focusedWorkId, gitRuntime, workSessions])
|
|
820
|
+
|
|
821
|
+
useEffect(
|
|
822
|
+
() =>
|
|
823
|
+
subscribeL2ProjectPreviewDirectories(
|
|
824
|
+
chatRuntime,
|
|
825
|
+
fileRuntime,
|
|
826
|
+
gitRuntime,
|
|
827
|
+
() => workSessionsRef.current,
|
|
828
|
+
() => focusedWorkIdRef.current
|
|
829
|
+
),
|
|
830
|
+
[chatRuntime, fileRuntime, gitRuntime]
|
|
831
|
+
)
|
|
816
832
|
|
|
817
833
|
useEffect(() => {
|
|
818
834
|
const token = {}
|
|
@@ -976,8 +992,22 @@ export function L2Workbench({
|
|
|
976
992
|
const displayed = getL2DisplayedWorkSessionIds(layout, pinnedWorkIds)
|
|
977
993
|
.map((workId) => workSessionById.get(workId))
|
|
978
994
|
.filter((workSession): workSession is L2WorkSessionListItem => workSession !== undefined)
|
|
995
|
+
if (fileBrowserMounted && activeWorkSession && sidebarMode === 'files') {
|
|
996
|
+
if (!displayed.some((session) => session.workId === activeWorkSession.workId)) {
|
|
997
|
+
displayed.push(activeWorkSession)
|
|
998
|
+
}
|
|
999
|
+
}
|
|
979
1000
|
chatRuntime.ensureDisplayedWorkSessions(displayed)
|
|
980
|
-
}, [
|
|
1001
|
+
}, [
|
|
1002
|
+
activeWorkSession,
|
|
1003
|
+
chatRuntime,
|
|
1004
|
+
fileBrowserMounted,
|
|
1005
|
+
layout,
|
|
1006
|
+
layoutLoaded,
|
|
1007
|
+
pinnedWorkIds,
|
|
1008
|
+
sidebarMode,
|
|
1009
|
+
workSessions
|
|
1010
|
+
])
|
|
981
1011
|
|
|
982
1012
|
useEffect(() => {
|
|
983
1013
|
if (!layoutLoaded) return
|
|
@@ -1513,7 +1543,7 @@ export function L2Workbench({
|
|
|
1513
1543
|
const handleSidebarRefreshFiles = useCallback((): void => {
|
|
1514
1544
|
if (!activeCwd || !activeWorkId) return
|
|
1515
1545
|
fileRuntime.refreshTree(activeWorkId)
|
|
1516
|
-
void gitRuntime.refreshSnapshot(
|
|
1546
|
+
void gitRuntime.refreshSnapshot(fileRuntime.getDirectory(activeWorkId))
|
|
1517
1547
|
}, [activeCwd, activeWorkId, fileRuntime, gitRuntime])
|
|
1518
1548
|
const openGitBranchPicker = useCallback((target: L2GitBranchPickerTarget): void => {
|
|
1519
1549
|
setGitBranchPickerTarget(target)
|
|
@@ -1553,6 +1583,7 @@ export function L2Workbench({
|
|
|
1553
1583
|
closeMobileDrawer()
|
|
1554
1584
|
return
|
|
1555
1585
|
}
|
|
1586
|
+
fileRuntime.selectDirectory(current.workId, current.cwd)
|
|
1556
1587
|
fileRuntime.openFile(current.workId, path)
|
|
1557
1588
|
focusWorkSession(current.workId)
|
|
1558
1589
|
closeMobileDrawer()
|
|
@@ -1851,7 +1882,7 @@ export function L2Workbench({
|
|
|
1851
1882
|
if (!activeCwd) return
|
|
1852
1883
|
closeMobileDrawer()
|
|
1853
1884
|
fileRuntime.collapseExpandedWindow()
|
|
1854
|
-
pluginHost.previewProject(activeCwd)
|
|
1885
|
+
pluginHost.previewProject(activeWorkId ? fileRuntime.getDirectory(activeWorkId) : activeCwd)
|
|
1855
1886
|
}}
|
|
1856
1887
|
onOpenTerminal={() => {
|
|
1857
1888
|
closeMobileDrawer()
|
|
@@ -91,6 +91,7 @@ interface DetailLoadState {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
const USER_MESSAGE_COLLAPSE_THRESHOLD_LINES = 6
|
|
94
|
+
const THINKING_RENDER_CHARACTER_LIMIT = 5_000
|
|
94
95
|
const MESSAGE_COPY_FEEDBACK_MS = 1500
|
|
95
96
|
const PROCESS_ITEM_BATCH_SIZE = 64
|
|
96
97
|
const TOOL_DETAIL_SCROLL_CLASS = 'max-h-[min(20rem,45dvh)] overflow-auto [scrollbar-gutter:stable]'
|
|
@@ -656,22 +657,40 @@ function AssistantReasoningContent({
|
|
|
656
657
|
content: string
|
|
657
658
|
streaming: boolean
|
|
658
659
|
}): React.JSX.Element {
|
|
660
|
+
const { t } = useTranslation('conversation')
|
|
659
661
|
const { scrollRef, contentRef } = useStickToBottom({
|
|
660
662
|
initial: streaming ? 'instant' : false,
|
|
661
663
|
resize: 'instant'
|
|
662
664
|
})
|
|
665
|
+
const truncated = content.length > THINKING_RENDER_CHARACTER_LIMIT
|
|
666
|
+
const visibleContent = content.slice(-THINKING_RENDER_CHARACTER_LIMIT)
|
|
663
667
|
|
|
664
668
|
return (
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
<
|
|
671
|
-
{
|
|
672
|
-
|
|
669
|
+
<>
|
|
670
|
+
<div
|
|
671
|
+
ref={scrollRef}
|
|
672
|
+
className="max-h-56 overflow-y-auto pb-2 pr-2 [scrollbar-gutter:stable] sm:max-h-72"
|
|
673
|
+
>
|
|
674
|
+
<div ref={contentRef}>
|
|
675
|
+
{/* Streamdown 按节点位置缓存元素;窗口移动时重建,避免等长内容沿用旧文本。 */}
|
|
676
|
+
<L3ConversationMarkdown
|
|
677
|
+
key={truncated ? content.length : undefined}
|
|
678
|
+
isAnimating={streaming}
|
|
679
|
+
mode={streaming ? 'streaming' : 'static'}
|
|
680
|
+
>
|
|
681
|
+
{visibleContent}
|
|
682
|
+
</L3ConversationMarkdown>
|
|
683
|
+
</div>
|
|
673
684
|
</div>
|
|
674
|
-
|
|
685
|
+
{truncated ? (
|
|
686
|
+
<div className="flex items-center justify-between gap-2 pt-1">
|
|
687
|
+
<p className="text-xs text-muted-foreground">
|
|
688
|
+
{t('thinkingTruncated', { count: THINKING_RENDER_CHARACTER_LIMIT })}
|
|
689
|
+
</p>
|
|
690
|
+
<MessageCopyAction text={content} copyLabel={t('copyThinking')} alwaysVisible />
|
|
691
|
+
</div>
|
|
692
|
+
) : null}
|
|
693
|
+
</>
|
|
675
694
|
)
|
|
676
695
|
}
|
|
677
696
|
|
|
@@ -64,6 +64,8 @@ interface L3ProjectFileBrowserProps {
|
|
|
64
64
|
gitRuntime: L3ProjectGitBrowser
|
|
65
65
|
onOpenGitBranches: (repositoryRoot: string) => void
|
|
66
66
|
onFileOpened?: () => void
|
|
67
|
+
rootTitle?: React.ReactNode
|
|
68
|
+
gitReadOnly?: boolean
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
const EMPTY_GIT_REPOSITORIES: readonly L4GitRepository[] = []
|
|
@@ -155,7 +157,9 @@ export function L3ProjectFileBrowser({
|
|
|
155
157
|
runtime,
|
|
156
158
|
gitRuntime,
|
|
157
159
|
onOpenGitBranches,
|
|
158
|
-
onFileOpened
|
|
160
|
+
onFileOpened,
|
|
161
|
+
rootTitle,
|
|
162
|
+
gitReadOnly = false
|
|
159
163
|
}: L3ProjectFileBrowserProps): React.JSX.Element {
|
|
160
164
|
const { t } = useTranslation('projectFiles')
|
|
161
165
|
const { locale } = useL4Region()
|
|
@@ -165,7 +169,8 @@ export function L3ProjectFileBrowser({
|
|
|
165
169
|
const gitWorkspace = gitRuntime.getWorkspace(cwd)
|
|
166
170
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
167
171
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
168
|
-
const
|
|
172
|
+
const searchOpen = workspace.searchOpen
|
|
173
|
+
const setSearchOpen = (open: boolean): void => runtime.setSearchOpen(open)
|
|
169
174
|
const [visibleEntries, setVisibleEntries] = useState<Record<string, number>>({})
|
|
170
175
|
const moreEntries = (key: string, total: number): React.ReactNode => {
|
|
171
176
|
const count = visibleEntries[key] ?? 300
|
|
@@ -548,14 +553,14 @@ export function L3ProjectFileBrowser({
|
|
|
548
553
|
<button
|
|
549
554
|
data-testid={`project-git-branch-${root || 'root'}`}
|
|
550
555
|
type="button"
|
|
551
|
-
disabled={repository.state !== 'ready'}
|
|
556
|
+
disabled={gitReadOnly || repository.state !== 'ready'}
|
|
552
557
|
className={cn(
|
|
553
558
|
buttonVariants({ variant: 'outline', size: 'sm' }),
|
|
554
559
|
'max-w-28 shrink gap-1 font-mono text-xs text-muted-foreground'
|
|
555
560
|
)}
|
|
556
561
|
title={
|
|
557
562
|
repository.state === 'ready'
|
|
558
|
-
? t('gitBranches')
|
|
563
|
+
? t(gitReadOnly ? 'gitBranchReadOnly' : 'gitBranches')
|
|
559
564
|
: selectL4LocalizedText(repository.message, locale)
|
|
560
565
|
}
|
|
561
566
|
onClick={() => onOpenGitBranches(root)}
|
|
@@ -667,28 +672,35 @@ export function L3ProjectFileBrowser({
|
|
|
667
672
|
runtime.canReveal ? (path, type) => runtime.revealFile(path, type) : undefined
|
|
668
673
|
}
|
|
669
674
|
trigger={
|
|
670
|
-
|
|
671
|
-
<
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
675
|
+
rootTitle ? (
|
|
676
|
+
<div className="flex min-w-0 flex-1">{rootTitle}</div>
|
|
677
|
+
) : (
|
|
678
|
+
<div tabIndex={0} className="flex min-w-0 flex-1 items-baseline gap-1">
|
|
679
|
+
<p className="shrink-0 truncate text-xs font-semibold" title={projectName}>
|
|
680
|
+
{projectName}
|
|
681
|
+
</p>
|
|
682
|
+
<span className="text-xs text-muted-foreground">·</span>
|
|
683
|
+
<p
|
|
684
|
+
className="min-w-0 truncate font-mono text-xs text-muted-foreground"
|
|
685
|
+
title={cwd}
|
|
686
|
+
>
|
|
687
|
+
{compactPath(cwd)}
|
|
688
|
+
</p>
|
|
689
|
+
</div>
|
|
690
|
+
)
|
|
679
691
|
}
|
|
680
692
|
/>
|
|
681
693
|
{singleProjectRootRepository ? (
|
|
682
694
|
<button
|
|
683
695
|
type="button"
|
|
684
|
-
disabled={singleProjectRootRepository.state !== 'ready'}
|
|
696
|
+
disabled={gitReadOnly || singleProjectRootRepository.state !== 'ready'}
|
|
685
697
|
className={cn(
|
|
686
698
|
buttonVariants({ variant: 'outline', size: 'sm' }),
|
|
687
699
|
'max-w-32 shrink gap-1 font-mono text-xs text-muted-foreground'
|
|
688
700
|
)}
|
|
689
701
|
title={
|
|
690
702
|
singleProjectRootRepository.state === 'ready'
|
|
691
|
-
? t('gitBranches')
|
|
703
|
+
? t(gitReadOnly ? 'gitBranchReadOnly' : 'gitBranches')
|
|
692
704
|
: selectL4LocalizedText(singleProjectRootRepository.message, locale)
|
|
693
705
|
}
|
|
694
706
|
onClick={() => onOpenGitBranches('')}
|
|
@@ -79,6 +79,7 @@ export interface L3ProjectFileState {
|
|
|
79
79
|
expandedPaths: Set<string>
|
|
80
80
|
treeScrollTop: number
|
|
81
81
|
search: L3FileSearch
|
|
82
|
+
searchOpen: boolean
|
|
82
83
|
documents: Map<string, L3FileDocument>
|
|
83
84
|
activeDiff: L3FileDiff | null
|
|
84
85
|
}
|
|
@@ -130,6 +131,7 @@ export class L3ProjectFileRuntime {
|
|
|
130
131
|
expandedPaths: new Set(),
|
|
131
132
|
treeScrollTop: 0,
|
|
132
133
|
search: { query: '', status: 'idle', matches: [], truncated: false, error: null },
|
|
134
|
+
searchOpen: false,
|
|
133
135
|
documents: new Map(),
|
|
134
136
|
activeDiff: null
|
|
135
137
|
}
|
|
@@ -159,6 +161,11 @@ export class L3ProjectFileRuntime {
|
|
|
159
161
|
return () => this.listeners.delete(listener)
|
|
160
162
|
}
|
|
161
163
|
getRevision = (): number => this.revision
|
|
164
|
+
setSearchOpen(open: boolean): void {
|
|
165
|
+
if (this.state.searchOpen === open) return
|
|
166
|
+
this.state.searchOpen = open
|
|
167
|
+
this.publish()
|
|
168
|
+
}
|
|
162
169
|
get canReveal(): boolean {
|
|
163
170
|
return Boolean(this.options.reveal)
|
|
164
171
|
}
|
|
@@ -8,7 +8,7 @@ const LEGACY_STORAGE_KEY = 'pi-super:project-preview:global'
|
|
|
8
8
|
const PreferencesSchema = z.object({
|
|
9
9
|
wrapLines: z.boolean().default(true),
|
|
10
10
|
diffViewMode: z.enum(['side-by-side', 'inline']).default('side-by-side'),
|
|
11
|
-
collapseUnchanged: z.boolean().default(
|
|
11
|
+
collapseUnchanged: z.boolean().default(false)
|
|
12
12
|
})
|
|
13
13
|
type CodePreferences = z.infer<typeof PreferencesSchema>
|
|
14
14
|
const DEFAULT_PREFERENCES = PreferencesSchema.parse({})
|