@jetcrab/pi-desk 1.1.24-dev → 1.1.28-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/package.json +2 -2
- package/src/app/project-preview/page.tsx +13 -0
- package/src/client/l1_entry/pages/l1-project-preview-page.tsx +1 -5
- package/src/client/l2_biz/project-preview/hooks/l2-use-project-preview.ts +25 -0
- package/src/client/l2_biz/project-preview/l2-project-preview-storage.ts +7 -7
- package/src/client/l2_biz/project-preview/l2-project-preview.tsx +8 -5
- package/src/client/l2_biz/project-preview/runtime/l2-project-history-runtime.ts +14 -11
- package/src/client/l2_biz/project-preview/views/l2-project-branch-select.tsx +347 -0
- package/src/client/l2_biz/project-preview/views/l2-project-change-tree.tsx +28 -11
- package/src/client/l2_biz/project-preview/views/l2-project-history-list.tsx +35 -8
- package/src/client/l2_biz/project-preview/views/l2-project-history-view.tsx +46 -48
- package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-runtime.ts +2 -1
- package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-view.tsx +6 -2
- package/src/client/l3_modules/conversation/l3-conversation-viewport.tsx +18 -8
- package/src/client/l3_modules/project-files/l3-project-document-view.tsx +7 -4
- package/src/client/l3_modules/project-files/l3-project-file-runtime.ts +181 -89
- package/src/client/l3_modules/project-files/l3-project-file-tabs.tsx +80 -28
- package/src/common/l2_biz/project-preview/l2-project-history-messages.ts +4 -2
- package/src/common/l2_biz/project-preview/l2-project-preview-navigation.ts +14 -0
- package/src/server/l4_foundation/process/l4-process-tree.js +159 -40
- 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/react-loadable-manifest.json +1 -1
- package/temp/build/pi-desk/release/.next/required-server-files.js +2 -2
- package/temp/build/pi-desk/release/.next/required-server-files.json +2 -2
- 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/login/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/page.js +2 -2
- package/temp/build/pi-desk/release/.next/server/app/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/project-preview/page.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/project-preview/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/27344.js +15 -0
- package/temp/build/pi-desk/release/.next/server/chunks/35985.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/static/chunks/15302-6e070e13fa65c863.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/19991.bcca96d396b83030.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/65232-ef4e5a4174f62faf.js +15 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/{page-ff44dcf27c2ba1c2.js → page-ce2ebf7d8ea7a330.js} +3 -3
- package/temp/build/pi-desk/release/.next/static/chunks/app/project-preview/page-038b6c14e03ed878.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/{webpack-48eecbb66781c006.js → webpack-c0b805df46fcf8b3.js} +1 -1
- package/temp/build/pi-desk/release/.next/static/css/c43b04c244a64fb4.css +1 -0
- package/temp/build/pi-desk/release/.next/trace-build +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/20460.js +0 -15
- package/temp/build/pi-desk/release/.next/static/chunks/15302-9c807afc6c60de0c.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/19991.8aa2e4b6df15fad6.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/9038-05fd2688407b39c1.js +0 -15
- package/temp/build/pi-desk/release/.next/static/chunks/app/project-preview/page-bc18765121950e65.js +0 -1
- package/temp/build/pi-desk/release/.next/static/css/10e063cfb3cda7bd.css +0 -1
- /package/temp/build/pi-desk/release/.next/static/{w3CeIboVupqJdq890TlcG → bj2dB_Z10YB_CqK_VNhDx}/_buildManifest.js +0 -0
- /package/temp/build/pi-desk/release/.next/static/{w3CeIboVupqJdq890TlcG → bj2dB_Z10YB_CqK_VNhDx}/_ssgManifest.js +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { ChevronRightIcon, Columns2Icon, FolderIcon, FolderOpenIcon } from 'lucide-react'
|
|
4
|
-
import type
|
|
4
|
+
import { useRef, type KeyboardEvent } from 'react'
|
|
5
5
|
import { useTranslation } from 'react-i18next'
|
|
6
6
|
import type { L4GitChanges } from '@common/l4_foundation/git/l4-git-history-contract'
|
|
7
7
|
import type { L3ProjectRevealTargetType } from '@common/l3_modules/project-files/l3-project-files-contract'
|
|
@@ -28,6 +28,7 @@ interface Props {
|
|
|
28
28
|
changes: L4GitChanges['items']
|
|
29
29
|
selectedPath: string | null
|
|
30
30
|
scrollTop: number
|
|
31
|
+
rowHeight: number
|
|
31
32
|
onScroll: (value: number) => void
|
|
32
33
|
onSelect: (path: string) => void
|
|
33
34
|
onToggle: (path: string) => void
|
|
@@ -42,6 +43,7 @@ export function L2ProjectChangeTree({
|
|
|
42
43
|
changes,
|
|
43
44
|
selectedPath,
|
|
44
45
|
scrollTop,
|
|
46
|
+
rowHeight,
|
|
45
47
|
onScroll,
|
|
46
48
|
onSelect,
|
|
47
49
|
onToggle,
|
|
@@ -49,6 +51,12 @@ export function L2ProjectChangeTree({
|
|
|
49
51
|
onReveal
|
|
50
52
|
}: Props): React.JSX.Element {
|
|
51
53
|
const { t } = useTranslation('projectHistory')
|
|
54
|
+
const pendingFocus = useRef<string | null>(null)
|
|
55
|
+
const focusRow = (element: HTMLElement | null, key: string): void => {
|
|
56
|
+
if (!element || pendingFocus.current !== key) return
|
|
57
|
+
pendingFocus.current = null
|
|
58
|
+
element.focus({ preventScroll: true })
|
|
59
|
+
}
|
|
52
60
|
const changesByPath = new Map(changes.map((change) => [change.path, change]))
|
|
53
61
|
const keyDown = (event: KeyboardEvent<HTMLDivElement>): void => {
|
|
54
62
|
if (event.defaultPrevented || !event.currentTarget.contains(event.target as Node)) return
|
|
@@ -84,14 +92,17 @@ export function L2ProjectChangeTree({
|
|
|
84
92
|
const target = rows[next]
|
|
85
93
|
if (!target) return
|
|
86
94
|
const viewport = event.currentTarget
|
|
87
|
-
if (next *
|
|
88
|
-
else if ((next + 1) *
|
|
89
|
-
viewport.scrollTop = (next + 1) *
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
if (next * rowHeight < viewport.scrollTop) viewport.scrollTop = next * rowHeight
|
|
96
|
+
else if ((next + 1) * rowHeight > viewport.scrollTop + viewport.clientHeight)
|
|
97
|
+
viewport.scrollTop = (next + 1) * rowHeight - viewport.clientHeight
|
|
98
|
+
pendingFocus.current = target.key
|
|
99
|
+
// 虚拟行可能尚未挂载,未命中时由该行的 ref 在提交后接续聚焦。
|
|
100
|
+
focusRow(
|
|
101
|
+
[...viewport.querySelectorAll<HTMLElement>('[data-tree-key]')].find(
|
|
102
|
+
(item) => item.dataset.treeKey === target.key
|
|
103
|
+
) ?? null,
|
|
104
|
+
target.key
|
|
105
|
+
)
|
|
95
106
|
}
|
|
96
107
|
|
|
97
108
|
return (
|
|
@@ -100,6 +111,7 @@ export function L2ProjectChangeTree({
|
|
|
100
111
|
label={t('changeFiles')}
|
|
101
112
|
items={rows}
|
|
102
113
|
scrollTop={scrollTop}
|
|
114
|
+
rowHeight={rowHeight}
|
|
103
115
|
onScroll={onScroll}
|
|
104
116
|
onKeyDown={keyDown}
|
|
105
117
|
render={(row) => {
|
|
@@ -125,6 +137,7 @@ export function L2ProjectChangeTree({
|
|
|
125
137
|
onReveal={onReveal}
|
|
126
138
|
trigger={
|
|
127
139
|
<div
|
|
140
|
+
ref={(element) => focusRow(element, row.key)}
|
|
128
141
|
role="treeitem"
|
|
129
142
|
tabIndex={0}
|
|
130
143
|
aria-level={row.depth + 1}
|
|
@@ -132,10 +145,13 @@ export function L2ProjectChangeTree({
|
|
|
132
145
|
aria-selected={row.type === 'file' && selectedPath === row.path}
|
|
133
146
|
data-tree-key={row.key}
|
|
134
147
|
className={cn(
|
|
135
|
-
'flex
|
|
148
|
+
'flex min-w-0 cursor-pointer items-center gap-1 pr-1 text-sm outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring',
|
|
136
149
|
row.type === 'file' && selectedPath === row.path && 'bg-accent'
|
|
137
150
|
)}
|
|
138
|
-
style={{
|
|
151
|
+
style={{
|
|
152
|
+
height: rowHeight,
|
|
153
|
+
paddingLeft: 4 + row.depth * (rowHeight === 24 ? 12 : 24)
|
|
154
|
+
}}
|
|
139
155
|
title={
|
|
140
156
|
change?.oldPath && row.type === 'file'
|
|
141
157
|
? `${joinL3ProjectPath(repositoryRoot, change.oldPath)} → ${projectPath}`
|
|
@@ -190,6 +206,7 @@ export function L2ProjectChangeTree({
|
|
|
190
206
|
<Button
|
|
191
207
|
size="icon-sm"
|
|
192
208
|
variant="ghost"
|
|
209
|
+
className={rowHeight === 24 ? 'size-6' : undefined}
|
|
193
210
|
aria-label={t('viewDiffPath', { path: row.path })}
|
|
194
211
|
onDoubleClick={(event) => event.stopPropagation()}
|
|
195
212
|
onClick={(event) => {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
useEffect,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
type KeyboardEventHandler
|
|
9
|
+
} from 'react'
|
|
3
10
|
|
|
4
11
|
export function L2ProjectHistoryList<T>({
|
|
5
12
|
items,
|
|
@@ -8,7 +15,10 @@ export function L2ProjectHistoryList<T>({
|
|
|
8
15
|
onScroll,
|
|
9
16
|
render,
|
|
10
17
|
role = 'listbox',
|
|
11
|
-
onKeyDown
|
|
18
|
+
onKeyDown,
|
|
19
|
+
rowHeight = 36,
|
|
20
|
+
onNearEnd,
|
|
21
|
+
footer
|
|
12
22
|
}: {
|
|
13
23
|
items: readonly T[]
|
|
14
24
|
label: string
|
|
@@ -17,6 +27,9 @@ export function L2ProjectHistoryList<T>({
|
|
|
17
27
|
render: (item: T, index: number) => ReactNode
|
|
18
28
|
role?: 'listbox' | 'tree'
|
|
19
29
|
onKeyDown?: KeyboardEventHandler<HTMLDivElement>
|
|
30
|
+
rowHeight?: number
|
|
31
|
+
onNearEnd?: () => void
|
|
32
|
+
footer?: ReactNode
|
|
20
33
|
}): React.JSX.Element {
|
|
21
34
|
const viewport = useRef<HTMLDivElement>(null)
|
|
22
35
|
const [window, setWindow] = useState({ top: scrollTop, height: 300 })
|
|
@@ -33,10 +46,23 @@ export function L2ProjectHistoryList<T>({
|
|
|
33
46
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
34
47
|
}, [])
|
|
35
48
|
useLayoutEffect(() => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
const element = viewport.current
|
|
50
|
+
if (!element) return
|
|
51
|
+
element.scrollTop = scrollTop
|
|
52
|
+
setWindow((previous) =>
|
|
53
|
+
previous.top === element.scrollTop ? previous : { ...previous, top: element.scrollTop }
|
|
54
|
+
)
|
|
55
|
+
}, [items, scrollTop, rowHeight])
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (
|
|
58
|
+
items.length &&
|
|
59
|
+
window.height > 0 &&
|
|
60
|
+
items.length * rowHeight - window.top - window.height <= 200
|
|
61
|
+
)
|
|
62
|
+
onNearEnd?.()
|
|
63
|
+
}, [items.length, rowHeight, window.top, window.height, onNearEnd])
|
|
64
|
+
const start = Math.max(0, Math.floor(window.top / rowHeight) - 8)
|
|
65
|
+
const end = Math.min(items.length, Math.ceil((window.top + window.height) / rowHeight) + 8)
|
|
40
66
|
return (
|
|
41
67
|
<div
|
|
42
68
|
ref={viewport}
|
|
@@ -51,11 +77,12 @@ export function L2ProjectHistoryList<T>({
|
|
|
51
77
|
onScroll(top)
|
|
52
78
|
}}
|
|
53
79
|
>
|
|
54
|
-
<div style={{ height: items.length *
|
|
55
|
-
<div style={{ position: 'absolute', top: start *
|
|
80
|
+
<div style={{ height: items.length * rowHeight, position: 'relative' }}>
|
|
81
|
+
<div style={{ position: 'absolute', top: start * rowHeight, left: 0, right: 0 }}>
|
|
56
82
|
{items.slice(start, end).map((item, index) => render(item, start + index))}
|
|
57
83
|
</div>
|
|
58
84
|
</div>
|
|
85
|
+
{footer}
|
|
59
86
|
</div>
|
|
60
87
|
)
|
|
61
88
|
}
|
|
@@ -18,6 +18,7 @@ import { cn } from '@client/l4_foundation/lib/l4-utils'
|
|
|
18
18
|
import type { L2ProjectHistoryState } from '../runtime/l2-project-history-runtime'
|
|
19
19
|
import { L2ProjectHistoryList } from './l2-project-history-list'
|
|
20
20
|
import { L2ProjectChangeTree } from './l2-project-change-tree'
|
|
21
|
+
import { L2ProjectBranchSelect } from './l2-project-branch-select'
|
|
21
22
|
import type { L3PathTreeRow } from '@client/l3_modules/project-files/l3-project-path-tree'
|
|
22
23
|
import type { L3ProjectRevealTargetType } from '@common/l3_modules/project-files/l3-project-files-contract'
|
|
23
24
|
|
|
@@ -25,6 +26,7 @@ interface Props {
|
|
|
25
26
|
state: L2ProjectHistoryState
|
|
26
27
|
cwd: string
|
|
27
28
|
changeRows: readonly L3PathTreeRow[]
|
|
29
|
+
changeRowHeight: number
|
|
28
30
|
onSelectChange: (path: string) => void
|
|
29
31
|
onToggleDirectory: (path: string) => void
|
|
30
32
|
onReveal: (path: string, type: L3ProjectRevealTargetType) => Promise<void>
|
|
@@ -86,30 +88,17 @@ export function L2ProjectHistoryView(props: Props): React.JSX.Element {
|
|
|
86
88
|
const data = state.changes.data
|
|
87
89
|
const context = `${state.repositoryRoot}:${data?.comparison.original}:${data?.comparison.target}`
|
|
88
90
|
const selectedPath = state.selectedChangePath
|
|
89
|
-
const refs = [
|
|
90
|
-
{ value: 'HEAD', label: t('currentHead') },
|
|
91
|
-
...(state.branches.data?.local ?? []).map((name) => ({
|
|
92
|
-
value: `refs/heads/${name}`,
|
|
93
|
-
label: name,
|
|
94
|
-
description: t('localBranch')
|
|
95
|
-
})),
|
|
96
|
-
...(state.branches.data?.remote ?? []).map((name) => ({
|
|
97
|
-
value: `refs/remotes/${name}`,
|
|
98
|
-
label: name,
|
|
99
|
-
description: t('remoteBranch')
|
|
100
|
-
}))
|
|
101
|
-
]
|
|
102
91
|
const upstream = state.branches.data?.upstream
|
|
103
92
|
const commit = data?.commit
|
|
104
93
|
const chooseCommit = (oid: string): void => {
|
|
105
94
|
props.onCommit(oid)
|
|
106
95
|
if (narrow) setMobilePane('files')
|
|
107
96
|
}
|
|
108
|
-
const feedback = (error: string | null): React.JSX.Element | null =>
|
|
97
|
+
const feedback = (error: string | null, retry = props.onRefresh): React.JSX.Element | null =>
|
|
109
98
|
error ? (
|
|
110
99
|
<div role="alert" className="flex flex-wrap items-center gap-2 p-3 text-sm text-destructive">
|
|
111
100
|
<span className="min-w-0 flex-1 break-words">{error}</span>
|
|
112
|
-
<Button size="sm" variant="outline" onClick={
|
|
101
|
+
<Button size="sm" variant="outline" onClick={retry}>
|
|
113
102
|
{t('retry')}
|
|
114
103
|
</Button>
|
|
115
104
|
{state.mode === 'compare' && state.strategy === 'merge-base' ? (
|
|
@@ -122,19 +111,47 @@ export function L2ProjectHistoryView(props: Props): React.JSX.Element {
|
|
|
122
111
|
|
|
123
112
|
const log = (
|
|
124
113
|
<div className="flex size-full min-h-0 min-w-0 flex-col">
|
|
125
|
-
{feedback(state.log.error)}
|
|
126
|
-
{state.log.status === 'loading' ? (
|
|
127
|
-
<div className="flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground">
|
|
128
|
-
<LoaderCircleIcon className="size-4 animate-spin" />
|
|
129
|
-
{t('loadingCommits')}
|
|
130
|
-
</div>
|
|
131
|
-
) : null}
|
|
132
114
|
<L2ProjectHistoryList
|
|
133
115
|
key={`${state.repositoryRoot}:${state.mode}:${state.tip}:${state.base}:${state.target}:${state.query}:${state.author}`}
|
|
134
116
|
items={state.log.data?.items ?? []}
|
|
135
117
|
label={t('commitList')}
|
|
136
118
|
scrollTop={scroll.log}
|
|
137
119
|
onScroll={(value) => props.onScroll('log', value)}
|
|
120
|
+
onNearEnd={
|
|
121
|
+
state.log.status === 'ready' &&
|
|
122
|
+
state.log.data?.hasMore &&
|
|
123
|
+
state.log.data.items.length < 2000
|
|
124
|
+
? props.onMoreLog
|
|
125
|
+
: undefined
|
|
126
|
+
}
|
|
127
|
+
footer={
|
|
128
|
+
<>
|
|
129
|
+
{feedback(state.log.error, state.log.data ? props.onMoreLog : props.onRefresh)}
|
|
130
|
+
{state.log.status === 'loading' ? (
|
|
131
|
+
<div
|
|
132
|
+
role="status"
|
|
133
|
+
className="flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground"
|
|
134
|
+
>
|
|
135
|
+
<LoaderCircleIcon className="size-4 animate-spin" />
|
|
136
|
+
{t('loadingCommits')}
|
|
137
|
+
</div>
|
|
138
|
+
) : null}
|
|
139
|
+
{state.log.status === 'ready' && !state.log.data?.items.length ? (
|
|
140
|
+
<p className="px-3 py-4 text-sm text-muted-foreground">
|
|
141
|
+
{state.query || state.author ? t('noMatchingCommits') : t('noCommits')}
|
|
142
|
+
</p>
|
|
143
|
+
) : null}
|
|
144
|
+
{state.log.data?.truncated ? (
|
|
145
|
+
<p className="px-3 py-2 text-xs text-muted-foreground">{t('scanLimit')}</p>
|
|
146
|
+
) : (state.log.data?.items.length ?? 0) >= 2000 ? (
|
|
147
|
+
<p className="px-3 py-2 text-xs text-muted-foreground">{t('loadedLimit')}</p>
|
|
148
|
+
) : state.log.status === 'ready' &&
|
|
149
|
+
state.log.data?.items.length &&
|
|
150
|
+
!state.log.data.hasMore ? (
|
|
151
|
+
<p className="px-3 py-2 text-xs text-muted-foreground">{t('noMoreCommits')}</p>
|
|
152
|
+
) : null}
|
|
153
|
+
</>
|
|
154
|
+
}
|
|
138
155
|
render={(item) => (
|
|
139
156
|
<button
|
|
140
157
|
key={item.oid}
|
|
@@ -168,26 +185,6 @@ export function L2ProjectHistoryView(props: Props): React.JSX.Element {
|
|
|
168
185
|
</button>
|
|
169
186
|
)}
|
|
170
187
|
/>
|
|
171
|
-
{state.log.status === 'ready' && !state.log.data?.items.length ? (
|
|
172
|
-
<p className="px-3 py-4 text-sm text-muted-foreground">
|
|
173
|
-
{state.query || state.author ? t('noMatchingCommits') : t('noCommits')}
|
|
174
|
-
</p>
|
|
175
|
-
) : null}
|
|
176
|
-
{state.log.data?.truncated ? (
|
|
177
|
-
<p className="px-3 py-2 text-xs text-muted-foreground">{t('scanLimit')}</p>
|
|
178
|
-
) : null}
|
|
179
|
-
{(state.log.data?.items.length ?? 0) >= 2000 ? (
|
|
180
|
-
<p className="px-3 py-2 text-xs text-muted-foreground">{t('loadedLimit')}</p>
|
|
181
|
-
) : state.log.data?.hasMore ? (
|
|
182
|
-
<Button
|
|
183
|
-
variant="ghost"
|
|
184
|
-
size="sm"
|
|
185
|
-
disabled={state.log.status === 'loading'}
|
|
186
|
-
onClick={props.onMoreLog}
|
|
187
|
-
>
|
|
188
|
-
{t('loadMoreCommits')}
|
|
189
|
-
</Button>
|
|
190
|
-
) : null}
|
|
191
188
|
</div>
|
|
192
189
|
)
|
|
193
190
|
|
|
@@ -247,6 +244,7 @@ export function L2ProjectHistoryView(props: Props): React.JSX.Element {
|
|
|
247
244
|
cwd={props.cwd}
|
|
248
245
|
repositoryRoot={state.repositoryRoot ?? ''}
|
|
249
246
|
rows={props.changeRows}
|
|
247
|
+
rowHeight={props.changeRowHeight}
|
|
250
248
|
changes={data?.items ?? []}
|
|
251
249
|
selectedPath={selectedPath}
|
|
252
250
|
scrollTop={scroll.changes}
|
|
@@ -387,29 +385,29 @@ export function L2ProjectHistoryView(props: Props): React.JSX.Element {
|
|
|
387
385
|
onChange={props.onRepository}
|
|
388
386
|
/>
|
|
389
387
|
{state.mode === 'history' ? (
|
|
390
|
-
<
|
|
388
|
+
<L2ProjectBranchSelect
|
|
391
389
|
className="w-48"
|
|
392
390
|
ariaLabel={t('viewBranch')}
|
|
393
391
|
value={state.tip}
|
|
394
|
-
|
|
392
|
+
branches={state.branches.data}
|
|
395
393
|
onChange={(value) => props.onRevision('tip', value)}
|
|
396
394
|
/>
|
|
397
395
|
) : (
|
|
398
396
|
<>
|
|
399
|
-
<
|
|
397
|
+
<L2ProjectBranchSelect
|
|
400
398
|
className="w-44"
|
|
401
399
|
ariaLabel={t('baseBranch')}
|
|
402
400
|
placeholder={t('chooseBase')}
|
|
403
401
|
value={state.base}
|
|
404
|
-
|
|
402
|
+
branches={state.branches.data}
|
|
405
403
|
onChange={(value) => props.onRevision('base', value)}
|
|
406
404
|
/>
|
|
407
405
|
<span className="text-xs text-muted-foreground">→</span>
|
|
408
|
-
<
|
|
406
|
+
<L2ProjectBranchSelect
|
|
409
407
|
className="w-44"
|
|
410
408
|
ariaLabel={t('targetBranch')}
|
|
411
409
|
value={state.target}
|
|
412
|
-
|
|
410
|
+
branches={state.branches.data}
|
|
413
411
|
onChange={(value) => props.onRevision('target', value)}
|
|
414
412
|
/>
|
|
415
413
|
<L4SearchSelect
|
|
@@ -9,6 +9,7 @@ import { readL3ImagePreviewMode } from '@client/l3_modules/preferences/l3-work-s
|
|
|
9
9
|
import {
|
|
10
10
|
L3ProjectFileRuntime,
|
|
11
11
|
type L3ProjectFileState,
|
|
12
|
+
type L3FileTabTarget,
|
|
12
13
|
type L3FileTabCloseScope
|
|
13
14
|
} from '@client/l3_modules/project-files/l3-project-file-runtime'
|
|
14
15
|
import type { L2WorkSessionGitBiz } from '../git-workspace/l2-work-session-git-biz'
|
|
@@ -222,7 +223,7 @@ export class L2FileWorkspaceRuntime {
|
|
|
222
223
|
closeFile(id: string, path: string): void {
|
|
223
224
|
this.closeTabs(id, path, 'current')
|
|
224
225
|
}
|
|
225
|
-
closeTabs(id: string, target:
|
|
226
|
+
closeTabs(id: string, target: L3FileTabTarget | null, scope: L3FileTabCloseScope): void {
|
|
226
227
|
this.workspaces.get(id)?.files.closeTabs(target, scope)
|
|
227
228
|
}
|
|
228
229
|
hideWindow(id: string): void {
|
|
@@ -214,9 +214,13 @@ export function L2FileWorkspaceView({
|
|
|
214
214
|
gitRuntime={gitRuntime}
|
|
215
215
|
paths={workspace.tabs}
|
|
216
216
|
activePath={activePath}
|
|
217
|
-
|
|
217
|
+
diffs={workspace.diffs}
|
|
218
|
+
activeDiff={activeDiff}
|
|
218
219
|
mobile={mobile}
|
|
219
|
-
onActivate={(
|
|
220
|
+
onActivate={(target) => {
|
|
221
|
+
if (typeof target === 'string') runtime.activateFile(workId, target)
|
|
222
|
+
else runtime.openDiff(workId, target.repositoryRoot, target.path)
|
|
223
|
+
}}
|
|
220
224
|
onClose={(path, scope) => runtime.closeTabs(workId, path, scope)}
|
|
221
225
|
/>
|
|
222
226
|
<L3ProjectDocumentView
|
|
@@ -235,37 +235,47 @@ export function L3ConversationTailSpacerObserver({ active = true }: { active?: b
|
|
|
235
235
|
scheduleRefresh()
|
|
236
236
|
})
|
|
237
237
|
let previousHeight: number | undefined
|
|
238
|
+
let previousWidth: number | undefined
|
|
238
239
|
let viewportDifference = 0
|
|
239
240
|
let resizeReleaseFrame: number | null = null
|
|
240
241
|
let resizeReleaseTimer: number | null = null
|
|
241
242
|
const viewportObserver = new ResizeObserver(([entry]) => {
|
|
242
|
-
|
|
243
|
-
|
|
243
|
+
if (!entry) return
|
|
244
|
+
const { height, width } = entry.contentRect
|
|
244
245
|
const difference = height - (previousHeight ?? height)
|
|
246
|
+
const widthDifference = width - (previousWidth ?? width)
|
|
245
247
|
previousHeight = height
|
|
246
|
-
|
|
248
|
+
previousWidth = width
|
|
249
|
+
if (!difference && !widthDifference) {
|
|
247
250
|
scheduleRefresh()
|
|
248
251
|
return
|
|
249
252
|
}
|
|
250
253
|
|
|
251
254
|
logL3ConversationScroll('viewport:resize', scrollElement, {
|
|
252
255
|
difference,
|
|
256
|
+
widthDifference,
|
|
253
257
|
isAtBottom: state.isAtBottom,
|
|
254
258
|
escapedFromLock: state.escapedFromLock
|
|
255
259
|
})
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
260
|
+
// 库只观察内容高度;仅宽度变化时保留库标记的内容高度差。
|
|
261
|
+
if (difference) {
|
|
262
|
+
viewportDifference = difference
|
|
263
|
+
state.resizeDifference = difference
|
|
264
|
+
}
|
|
259
265
|
if (refreshFrame !== null) {
|
|
260
266
|
window.cancelAnimationFrame(refreshFrame)
|
|
261
267
|
refreshFrame = null
|
|
262
268
|
}
|
|
263
|
-
//
|
|
269
|
+
// 宽度重排和高度变化都在绘制前同步跟底;库的 instant 仍会等到下一帧。
|
|
264
270
|
refreshL3ConversationTailSpacer(scrollElement)
|
|
265
271
|
if (state.isAtBottom) {
|
|
266
272
|
state.scrollTop = Math.max(0, state.calculatedTargetScrollTop)
|
|
267
273
|
}
|
|
268
|
-
logL3ConversationScroll('viewport:resize:positioned', scrollElement, {
|
|
274
|
+
logL3ConversationScroll('viewport:resize:positioned', scrollElement, {
|
|
275
|
+
difference,
|
|
276
|
+
widthDifference
|
|
277
|
+
})
|
|
278
|
+
if (!difference) return
|
|
269
279
|
if (resizeReleaseFrame !== null) window.cancelAnimationFrame(resizeReleaseFrame)
|
|
270
280
|
if (resizeReleaseTimer !== null) window.clearTimeout(resizeReleaseTimer)
|
|
271
281
|
resizeReleaseFrame = window.requestAnimationFrame(() => {
|
|
@@ -33,7 +33,7 @@ import { useL4CodePreferences } from '@client/l4_foundation/ui/code/l4-code-pref
|
|
|
33
33
|
import { L4ImageViewer } from '@client/l4_foundation/ui/l4-image-viewer'
|
|
34
34
|
import { useL4AppToast } from '@client/l4_foundation/ui/l4-app-toast'
|
|
35
35
|
import { Button } from '@client/l4_foundation/ui/shadcn/button'
|
|
36
|
-
import type
|
|
36
|
+
import { getL3FileDiffKey, type L3ProjectFileRuntime } from './l3-project-file-runtime'
|
|
37
37
|
import type { L3ProjectGitBrowser } from './l3-project-git-runtime'
|
|
38
38
|
import { useL3ProjectDocumentGit } from './l3-project-document-git'
|
|
39
39
|
import { L3CodeSurfaceToolbarButton } from './l3-code-surface-toolbar-button'
|
|
@@ -158,7 +158,10 @@ export function L3ProjectDocumentView({
|
|
|
158
158
|
{activeDiff ? (
|
|
159
159
|
<>
|
|
160
160
|
<span className="mr-auto shrink-0 px-1 text-xs text-muted-foreground">
|
|
161
|
-
{activeDiff.label ??
|
|
161
|
+
{activeDiff.label ??
|
|
162
|
+
(activeDiff.comparison
|
|
163
|
+
? `${activeDiff.comparison.baseCommit?.slice(0, 7) ?? '∅'} → ${activeDiff.comparison.targetCommit.slice(0, 7)}`
|
|
164
|
+
: t('headToWorking'))}
|
|
162
165
|
{activeDiff.status === 'text' &&
|
|
163
166
|
activeDiff.original.content === activeDiff.modified.content
|
|
164
167
|
? t('noDiffChanges')
|
|
@@ -333,7 +336,7 @@ export function L3ProjectDocumentView({
|
|
|
333
336
|
{activeDiff ? (
|
|
334
337
|
activeDiff.status === 'text' ? (
|
|
335
338
|
<L4CodeDiff
|
|
336
|
-
key={
|
|
339
|
+
key={getL3FileDiffKey(activeDiff)}
|
|
337
340
|
ref={diffRef}
|
|
338
341
|
original={activeDiff.original}
|
|
339
342
|
modified={activeDiff.modified}
|
|
@@ -352,7 +355,7 @@ export function L3ProjectDocumentView({
|
|
|
352
355
|
{activeDiff.modifiedMetadata}
|
|
353
356
|
</pre>
|
|
354
357
|
</div>
|
|
355
|
-
) : activeDiff.status === 'loading' ? (
|
|
358
|
+
) : activeDiff.status === 'loading' || activeDiff.status === 'unloaded' ? (
|
|
356
359
|
<Loading />
|
|
357
360
|
) : (
|
|
358
361
|
<div className="flex size-full flex-col items-center justify-center gap-3 p-6 text-center text-sm">
|