@jetcrab/pi-desk 1.1.23-dev → 1.1.25
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/commands.md +4 -2
- package/docs/plugins.md +2 -0
- 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/l1_entry/api/l1-plugin-management-route.ts +0 -4
- package/src/server/l2_biz/plugin-management/l2-plugin-management.ts +4 -37
- package/src/server/l2_biz/plugin-management/l2-plugin-updates.ts +1 -4
- package/src/server/l2_biz/work-session/l2-work-session-chat-runtime.ts +4 -2
- package/src/server/l4_foundation/pi/l4-pi-chat-worker.ts +16 -1
- package/src/server/l4_foundation/pi/l4-pi-package-root-consistency.ts +3 -6
- package/src/server/l4_foundation/pi/l4-pi-package-root-gate.ts +1 -1
- package/src/server/l4_foundation/pi/l4-pi-plugin-owner-runtime.ts +4 -0
- package/src/server/l4_foundation/pi/l4-pi-plugin-sources.ts +5 -1
- package/src/server/l4_foundation/pi/l4-pi-session-initialization.ts +30 -34
- package/src/server/l4_foundation/pi/l4-pi-session-resources.ts +15 -14
- package/src/server/l4_foundation/pi/l4-pi-work-session-runtime.ts +1 -1
- package/src/server/l4_foundation/skills/runtime/l4-skill-catalog.ts +159 -166
- 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/health/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/plugins/add/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/plugins/apply/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/plugins/batch/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/plugins/del/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/plugins/enabled-set/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/plugins/get/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/plugins/list/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/plugins/reload/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/plugins/update/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/16438.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/18748.js +1 -0
- package/temp/build/pi-desk/release/.next/server/chunks/24557.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/25406.js +13 -14
- 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/chunks/36361.js +2 -2
- package/temp/build/pi-desk/release/.next/server/chunks/50520.js +2 -2
- package/temp/build/pi-desk/release/.next/server/chunks/6917.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/80290.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/92386.js +15 -16
- 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/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-54040b9fd54ee8b3.js → page-18d63fd3920dfae0.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/server/chunks/93049.js +0 -1
- 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/{gAYTx4KJJR3vTtGYexj4J → 6a4eIEjmpW5l8Qlgtzzzg}/_buildManifest.js +0 -0
- /package/temp/build/pi-desk/release/.next/static/{gAYTx4KJJR3vTtGYexj4J → 6a4eIEjmpW5l8Qlgtzzzg}/_ssgManifest.js +0 -0
package/docs/commands.md
CHANGED
|
@@ -71,6 +71,8 @@
|
|
|
71
71
|
|
|
72
72
|
接纳后结束当前轮;宿主等本会话完全空闲后重载,再通知并唤醒本会话。重载可能重建插件内存状态、使缓存失效,配置和工具声明变化也可能影响提示词缓存。
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
插件管理最多等待目标来源的在途调用 15 秒;超过时仅该项维护失败,可稍后手动重试,不冻结常规聊天或阻止后续其他来源维护。完成通知表示包和宿主能力已应用;新建会话使用最新工具配置,已有原生 Pi 会话不自动重载。Node/Browser 替换会重新挂载目标插件界面,旧调用可能失效;结果未知的副作用请求不自动重发。
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
维护期间或失败后允许目标包暂时缺失。会话跳过缺失或版本不符的包,明确的单插件加载/启动错误在插件管理中保留诊断,不因此让其他正常插件一起降级。聊天订阅、刷新和重连不等待包维护或插件初始化;未就绪的模型与插件状态稍后同步。修复包后按需重载目标会话,不自动重载已有会话。
|
|
77
|
+
|
|
78
|
+
需要重启时按插件管理界面的维护流程处理,只延后受影响来源,以最终结果为准。
|
package/docs/plugins.md
CHANGED
|
@@ -339,4 +339,6 @@ try {
|
|
|
339
339
|
|
|
340
340
|
## 更新生效
|
|
341
341
|
|
|
342
|
+
包维护不阻塞聊天订阅、刷新或其他会话。缺失或版本不符的包会被跳过,导入/工厂失败的入口不加载;Pi 生命周期回调和插件 Provider 注册的明确错误保留在插件管理诊断中,不升级为整会话失败。正常扩展继续加载。此隔离不终止同步死循环,也不回滚插件已发生的外部副作用;整体初始化超时仍可能使当前会话回退基础聊天,不阻塞其他会话的历史同步。
|
|
343
|
+
|
|
342
344
|
`plugins install/reload` 应用 Node/Browser 能力;目标插件活动视图重挂,旧 Browser Owner 失效。现有 Native 会话不自动重载,新 UI 可能连接旧会话方法,需兼容既有消息和方法数据;需要现有会话加载当前原生代码时使用 `session reload`。热加载无法处理的资源按宿主已有重启流程处理,不把重新挂载界面当作后端任务已停止。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jetcrab/pi-desk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.25",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"viewerjs": "^1.11.8",
|
|
71
71
|
"ws": "^8.21.3",
|
|
72
72
|
"zod": "^4.4.3",
|
|
73
|
-
"@jetcrab/pi-desk-sdk": "^1.1.
|
|
73
|
+
"@jetcrab/pi-desk-sdk": "^1.1.10"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@earendil-works/pi-agent-core": "1.2.0",
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { Suspense } from 'react'
|
|
2
|
+
import type { Metadata } from 'next'
|
|
2
3
|
import { L1ProjectPreviewPage } from '@client/l1_entry/pages/l1-project-preview-page'
|
|
4
|
+
import { buildL2ProjectPreviewTitle } from '@common/l2_biz/project-preview/l2-project-preview-navigation'
|
|
5
|
+
import { getL4HostSettingsStore } from '@server/l4_foundation/l4-host-settings-store'
|
|
6
|
+
|
|
7
|
+
export async function generateMetadata({
|
|
8
|
+
searchParams
|
|
9
|
+
}: {
|
|
10
|
+
searchParams: Promise<{ cwd?: string | string[] }>
|
|
11
|
+
}): Promise<Metadata> {
|
|
12
|
+
const { cwd } = await searchParams
|
|
13
|
+
const { region } = getL4HostSettingsStore().getSnapshot()
|
|
14
|
+
return { title: buildL2ProjectPreviewTitle(Array.isArray(cwd) ? cwd[0] : cwd, region.locale) }
|
|
15
|
+
}
|
|
3
16
|
|
|
4
17
|
export default function ProjectPreview(): React.JSX.Element {
|
|
5
18
|
return (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useCallback,
|
|
3
|
+
import { useCallback, useRef } from 'react'
|
|
4
4
|
import { useRouter, useSearchParams } from 'next/navigation'
|
|
5
5
|
import { L2BuildLoginHref } from '@common/l2_biz/auth/l2-auth-return-to'
|
|
6
6
|
import { buildL2ProjectPreviewHref } from '@common/l2_biz/project-preview/l2-project-preview-navigation'
|
|
@@ -18,10 +18,6 @@ export function L1ProjectPreviewPage(): React.JSX.Element {
|
|
|
18
18
|
redirecting.current = true
|
|
19
19
|
window.location.assign(L2BuildLoginHref(window.location.pathname + window.location.search))
|
|
20
20
|
}, [])
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
const name = cwd?.replaceAll('\\', '/').split('/').filter(Boolean).at(-1)
|
|
23
|
-
document.title = name ? `${name} · Pi Desk` : 'Pi Desk'
|
|
24
|
-
}, [cwd])
|
|
25
21
|
return (
|
|
26
22
|
<L2ProjectPreviewPage
|
|
27
23
|
clientId={clientId}
|
|
@@ -12,6 +12,18 @@ import {
|
|
|
12
12
|
type L2ProjectPreviewProject
|
|
13
13
|
} from '../l2-project-preview-storage'
|
|
14
14
|
|
|
15
|
+
function subscribePointer(listener: () => void): () => void {
|
|
16
|
+
const media = window.matchMedia('(pointer: fine)')
|
|
17
|
+
media.addEventListener('change', listener)
|
|
18
|
+
return () => media.removeEventListener('change', listener)
|
|
19
|
+
}
|
|
20
|
+
function readFinePointer(): boolean {
|
|
21
|
+
return window.matchMedia('(pointer: fine)').matches
|
|
22
|
+
}
|
|
23
|
+
function readServerPointer(): boolean {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
type Page = 'files' | 'code' | 'git'
|
|
16
28
|
type HistoryPane = 'log' | 'files'
|
|
17
29
|
interface PreviewState extends Pick<L2ProjectPreviewOwner, 'files' | 'git' | 'history'> {
|
|
@@ -25,6 +37,7 @@ interface PreviewState extends Pick<L2ProjectPreviewOwner, 'files' | 'git' | 'hi
|
|
|
25
37
|
narrow: boolean
|
|
26
38
|
stacked: boolean
|
|
27
39
|
width: number
|
|
40
|
+
changeRowHeight: number
|
|
28
41
|
openGit: (root?: string) => void
|
|
29
42
|
refresh: () => void
|
|
30
43
|
}
|
|
@@ -67,6 +80,12 @@ export function useL2ProjectPreview(
|
|
|
67
80
|
const stacked = narrow || size.height < 680
|
|
68
81
|
const repositories = git.getWorkspace(cwd)!.snapshot.repositories
|
|
69
82
|
const state = files.state
|
|
83
|
+
const finePointer = useSyncExternalStore(subscribePointer, readFinePointer, readServerPointer)
|
|
84
|
+
const changeRowHeight = finePointer ? 24 : 36
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
history.setChangeRowHeight(changeRowHeight)
|
|
88
|
+
}, [history, changeRowHeight])
|
|
70
89
|
|
|
71
90
|
useEffect(() => {
|
|
72
91
|
saveCurrent.current = (): void => {
|
|
@@ -97,6 +116,11 @@ export function useL2ProjectPreview(
|
|
|
97
116
|
logScroll: history.scroll.log,
|
|
98
117
|
changesScroll: history.scroll.changes
|
|
99
118
|
},
|
|
119
|
+
diffs: files.state.diffs.map(({ repositoryRoot, path, comparison }) => ({
|
|
120
|
+
repositoryRoot,
|
|
121
|
+
path,
|
|
122
|
+
...(comparison ? { comparison } : {})
|
|
123
|
+
})),
|
|
100
124
|
activeDiff: files.state.activeDiff
|
|
101
125
|
? {
|
|
102
126
|
repositoryRoot: files.state.activeDiff.repositoryRoot,
|
|
@@ -198,6 +222,7 @@ export function useL2ProjectPreview(
|
|
|
198
222
|
narrow,
|
|
199
223
|
stacked,
|
|
200
224
|
width: size.width,
|
|
225
|
+
changeRowHeight,
|
|
201
226
|
openGit(root): void {
|
|
202
227
|
updateGlobal({ gitOpen: true })
|
|
203
228
|
setPage('git')
|
|
@@ -57,6 +57,11 @@ const editorStateSchema = z.custom<L4CodePreviewState>(
|
|
|
57
57
|
Array.isArray((value as { cursorState?: unknown }).cursorState) &&
|
|
58
58
|
typeof (value as { viewState?: unknown }).viewState === 'object'
|
|
59
59
|
)
|
|
60
|
+
const diffSchema = z.object({
|
|
61
|
+
repositoryRoot: z.string(),
|
|
62
|
+
path: L4ProjectFilePathSchema,
|
|
63
|
+
comparison: L4GitHistoricalDiffSchema.optional()
|
|
64
|
+
})
|
|
60
65
|
const projectSchema = z.object({
|
|
61
66
|
page: z.enum(['files', 'code', 'git']),
|
|
62
67
|
historyPane: z.enum(['log', 'files']),
|
|
@@ -70,13 +75,8 @@ const projectSchema = z.object({
|
|
|
70
75
|
changesOnly: z.boolean(),
|
|
71
76
|
expandedRepositories: z.array(z.string()).max(100),
|
|
72
77
|
history: historySchema,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
repositoryRoot: z.string(),
|
|
76
|
-
path: L4ProjectFilePathSchema,
|
|
77
|
-
comparison: L4GitHistoricalDiffSchema.optional()
|
|
78
|
-
})
|
|
79
|
-
.nullable()
|
|
78
|
+
diffs: z.array(diffSchema).max(20).optional(),
|
|
79
|
+
activeDiff: diffSchema.nullable()
|
|
80
80
|
})
|
|
81
81
|
export type L2ProjectPreviewProject = z.infer<typeof projectSchema>
|
|
82
82
|
const entrySchema = z.object({ cwd: z.string(), lastOpenedAt: z.number(), state: projectSchema })
|
|
@@ -63,6 +63,7 @@ export function L2ProjectPreview({
|
|
|
63
63
|
narrow,
|
|
64
64
|
stacked,
|
|
65
65
|
width,
|
|
66
|
+
changeRowHeight,
|
|
66
67
|
openGit,
|
|
67
68
|
refresh
|
|
68
69
|
} = useL2ProjectPreview(clientId, cwd, refreshOnFocus, onUnauthorized)
|
|
@@ -71,9 +72,6 @@ export function L2ProjectPreview({
|
|
|
71
72
|
const gitState = git.getWorkspace(cwd)!
|
|
72
73
|
const repositories = gitState.snapshot.repositories
|
|
73
74
|
const state = files.state
|
|
74
|
-
const diffPath = state.activeDiff
|
|
75
|
-
? [state.activeDiff.repositoryRoot, state.activeDiff.path].filter(Boolean).join('/')
|
|
76
|
-
: null
|
|
77
75
|
const fileTree = (
|
|
78
76
|
<L3ProjectFileBrowser
|
|
79
77
|
cwd={cwd}
|
|
@@ -98,10 +96,14 @@ export function L2ProjectPreview({
|
|
|
98
96
|
gitRuntime={git}
|
|
99
97
|
paths={state.tabs}
|
|
100
98
|
activePath={state.activePath}
|
|
101
|
-
|
|
99
|
+
diffs={state.diffs}
|
|
100
|
+
activeDiff={state.activeDiff}
|
|
102
101
|
mobile={narrow}
|
|
103
102
|
actions={actions}
|
|
104
|
-
onActivate={(
|
|
103
|
+
onActivate={(target) => {
|
|
104
|
+
if (typeof target === 'string') files.openFile(target)
|
|
105
|
+
else files.openDiff(target.repositoryRoot, target.path, target.comparison)
|
|
106
|
+
}}
|
|
105
107
|
onClose={(path, scope) => files.closeTabs(path, scope)}
|
|
106
108
|
/>
|
|
107
109
|
)}
|
|
@@ -143,6 +145,7 @@ export function L2ProjectPreview({
|
|
|
143
145
|
state={history.state}
|
|
144
146
|
cwd={cwd}
|
|
145
147
|
changeRows={history.getChangeRows()}
|
|
148
|
+
changeRowHeight={changeRowHeight}
|
|
146
149
|
onSelectChange={(path) => history.selectChange(path)}
|
|
147
150
|
onToggleDirectory={(path) => history.toggleChangeDirectory(path)}
|
|
148
151
|
onReveal={(path, type) => files.revealFile(path, type)}
|
|
@@ -86,6 +86,7 @@ export class L2ProjectHistoryRuntime {
|
|
|
86
86
|
private readonly collapsedDirectories = new Set<string>()
|
|
87
87
|
private changeTree = buildL3ProjectPathTree([])
|
|
88
88
|
private changeRows: L3PathTreeRow[] = []
|
|
89
|
+
private changeRowHeight = 36
|
|
89
90
|
constructor(
|
|
90
91
|
private readonly reader: L3ProjectReader,
|
|
91
92
|
private readonly files: L3ProjectFileRuntime
|
|
@@ -137,6 +138,13 @@ export class L2ProjectHistoryRuntime {
|
|
|
137
138
|
this.scroll[pane] = value
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
setChangeRowHeight(height: number): void {
|
|
142
|
+
if (this.changeRowHeight === height) return
|
|
143
|
+
this.scroll.changes = (this.scroll.changes / this.changeRowHeight) * height
|
|
144
|
+
this.changeRowHeight = height
|
|
145
|
+
this.publish()
|
|
146
|
+
}
|
|
147
|
+
|
|
140
148
|
setRepository(root: string): void {
|
|
141
149
|
if (root === this.state.repositoryRoot) {
|
|
142
150
|
if (this.state.branches.status === 'idle') {
|
|
@@ -213,18 +221,16 @@ export class L2ProjectHistoryRuntime {
|
|
|
213
221
|
const oid = typeof commit === 'string' ? commit : commit.oid
|
|
214
222
|
this.state.selectedCommit = oid
|
|
215
223
|
this.scroll.changes = 0
|
|
216
|
-
this.clearHistoricalDiff()
|
|
217
224
|
void this.loadChanges({ commit: oid, ...(parent ? { parent } : {}) }, 1)
|
|
218
225
|
}
|
|
219
226
|
showAggregate(): void {
|
|
220
227
|
if (!this.range) return
|
|
221
228
|
this.state.selectedCommit = null
|
|
222
|
-
this.clearHistoricalDiff()
|
|
223
229
|
void this.loadChanges(this.range, 1)
|
|
224
230
|
}
|
|
225
231
|
loadMoreLog(): void {
|
|
226
232
|
if (
|
|
227
|
-
this.state.log.status !== 'ready' ||
|
|
233
|
+
(this.state.log.status !== 'ready' && this.state.log.status !== 'error') ||
|
|
228
234
|
!this.state.log.data?.hasMore ||
|
|
229
235
|
this.state.log.data.items.length >= 2000
|
|
230
236
|
)
|
|
@@ -416,7 +422,6 @@ export class L2ProjectHistoryRuntime {
|
|
|
416
422
|
this.state.log = empty()
|
|
417
423
|
this.state.changes = empty()
|
|
418
424
|
this.resetChangeTree()
|
|
419
|
-
this.clearHistoricalDiff()
|
|
420
425
|
this.publish()
|
|
421
426
|
}
|
|
422
427
|
private resetChangeTree(): void {
|
|
@@ -427,18 +432,16 @@ export class L2ProjectHistoryRuntime {
|
|
|
427
432
|
this.scroll.changes = 0
|
|
428
433
|
}
|
|
429
434
|
private rebuildChangeRows(): void {
|
|
430
|
-
const
|
|
435
|
+
const height = this.changeRowHeight
|
|
436
|
+
const index = Math.floor(this.scroll.changes / height)
|
|
431
437
|
const anchor = this.changeRows[index]
|
|
432
|
-
const offset = this.scroll.changes %
|
|
438
|
+
const offset = this.scroll.changes % height
|
|
433
439
|
this.changeRows = flattenL3ProjectPathTree(this.changeTree, this.collapsedDirectories)
|
|
434
440
|
const next = anchor ? this.changeRows.findIndex((row) => row.key === anchor.key) : -1
|
|
435
441
|
this.scroll.changes =
|
|
436
442
|
next >= 0
|
|
437
|
-
? next *
|
|
438
|
-
: Math.min(this.scroll.changes, Math.max(0, (this.changeRows.length - 1) *
|
|
439
|
-
}
|
|
440
|
-
private clearHistoricalDiff(): void {
|
|
441
|
-
if (this.files.state.activeDiff?.comparison) this.files.closeDiff()
|
|
443
|
+
? next * height + offset
|
|
444
|
+
: Math.min(this.scroll.changes, Math.max(0, (this.changeRows.length - 1) * height))
|
|
442
445
|
}
|
|
443
446
|
private begin(key: 'branches' | 'log' | 'changes'): AbortController {
|
|
444
447
|
this.cancel(key)
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useId, useMemo, useRef, useState, type KeyboardEvent } from 'react'
|
|
4
|
+
import { useTranslation } from 'react-i18next'
|
|
5
|
+
import { CheckIcon, ChevronDownIcon, ChevronRightIcon, ChevronsUpDownIcon } from 'lucide-react'
|
|
6
|
+
import type { L4GitReadBranches } from '@common/l4_foundation/git/l4-git-history-contract'
|
|
7
|
+
import {
|
|
8
|
+
buildL3GitBranchTree,
|
|
9
|
+
type L3GitBranchTreeNode
|
|
10
|
+
} from '@client/l3_modules/project-files/l3-project-git-model'
|
|
11
|
+
import { cn } from '@client/l4_foundation/lib/l4-utils'
|
|
12
|
+
import { Button } from '@client/l4_foundation/ui/shadcn/button'
|
|
13
|
+
import { Input } from '@client/l4_foundation/ui/shadcn/input'
|
|
14
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@client/l4_foundation/ui/shadcn/popover'
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
branches: L4GitReadBranches | null
|
|
18
|
+
value: string
|
|
19
|
+
onChange: (value: string) => void
|
|
20
|
+
ariaLabel: string
|
|
21
|
+
placeholder?: string
|
|
22
|
+
className?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface BranchRow {
|
|
26
|
+
key: string
|
|
27
|
+
label: string
|
|
28
|
+
fullName: string
|
|
29
|
+
parentKey: string | null
|
|
30
|
+
level: number
|
|
31
|
+
position: number
|
|
32
|
+
siblingCount: number
|
|
33
|
+
expandable: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function branchAncestors(value: string): string[] {
|
|
37
|
+
const result: string[] = []
|
|
38
|
+
let parent = value
|
|
39
|
+
while (parent.includes('/')) {
|
|
40
|
+
parent = parent.slice(0, parent.lastIndexOf('/'))
|
|
41
|
+
if (parent === 'refs') break
|
|
42
|
+
result.push(parent)
|
|
43
|
+
}
|
|
44
|
+
return result
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function L2ProjectBranchSelect({
|
|
48
|
+
branches,
|
|
49
|
+
value,
|
|
50
|
+
onChange,
|
|
51
|
+
ariaLabel,
|
|
52
|
+
placeholder,
|
|
53
|
+
className
|
|
54
|
+
}: Props): React.JSX.Element {
|
|
55
|
+
const { t } = useTranslation('projectHistory')
|
|
56
|
+
const { t: common } = useTranslation('common')
|
|
57
|
+
const treeId = useId()
|
|
58
|
+
const searchRef = useRef<HTMLInputElement>(null)
|
|
59
|
+
const triggerRef = useRef<HTMLButtonElement>(null)
|
|
60
|
+
const rowRefs = useRef(new Map<string, HTMLButtonElement>())
|
|
61
|
+
const [open, setOpen] = useState(false)
|
|
62
|
+
const [query, setQuery] = useState('')
|
|
63
|
+
const [activeKey, setActiveKey] = useState(value)
|
|
64
|
+
const [expanded, setExpanded] = useState(
|
|
65
|
+
() => new Set(['refs/heads', 'refs/remotes', ...branchAncestors(value)])
|
|
66
|
+
)
|
|
67
|
+
const search = query.trim().toLocaleLowerCase()
|
|
68
|
+
const selectedLabel =
|
|
69
|
+
value === 'HEAD' ? t('currentHead') : value.replace(/^refs\/(heads|remotes)\//, '')
|
|
70
|
+
|
|
71
|
+
const rows = useMemo((): BranchRow[] => {
|
|
72
|
+
const result: BranchRow[] = []
|
|
73
|
+
const groups = [
|
|
74
|
+
{ key: 'refs/heads', label: t('localBranch'), names: branches?.local ?? [] },
|
|
75
|
+
{ key: 'refs/remotes', label: t('remoteBranch'), names: branches?.remote ?? [] }
|
|
76
|
+
]
|
|
77
|
+
.map((group) => ({
|
|
78
|
+
...group,
|
|
79
|
+
nodes: buildL3GitBranchTree(
|
|
80
|
+
group.names.filter((name) => name.toLocaleLowerCase().includes(search))
|
|
81
|
+
)
|
|
82
|
+
}))
|
|
83
|
+
.filter((group) => group.nodes.length > 0)
|
|
84
|
+
const showHead = `HEAD ${t('currentHead')}`.toLocaleLowerCase().includes(search)
|
|
85
|
+
const rootCount = groups.length + Number(showHead)
|
|
86
|
+
if (showHead) {
|
|
87
|
+
result.push({
|
|
88
|
+
key: 'HEAD',
|
|
89
|
+
label: t('currentHead'),
|
|
90
|
+
fullName: t('currentHead'),
|
|
91
|
+
parentKey: null,
|
|
92
|
+
level: 1,
|
|
93
|
+
position: 1,
|
|
94
|
+
siblingCount: rootCount,
|
|
95
|
+
expandable: false
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
const append = (
|
|
99
|
+
nodes: readonly L3GitBranchTreeNode[],
|
|
100
|
+
prefix: string,
|
|
101
|
+
parentKey: string,
|
|
102
|
+
level: number
|
|
103
|
+
): void => {
|
|
104
|
+
nodes.forEach((node, index) => {
|
|
105
|
+
const key = `${prefix}/${node.path}`
|
|
106
|
+
result.push({
|
|
107
|
+
key,
|
|
108
|
+
label: node.name,
|
|
109
|
+
fullName: node.path,
|
|
110
|
+
parentKey,
|
|
111
|
+
level,
|
|
112
|
+
position: index + 1,
|
|
113
|
+
siblingCount: nodes.length,
|
|
114
|
+
expandable: node.children.length > 0
|
|
115
|
+
})
|
|
116
|
+
if (expanded.has(key)) append(node.children, prefix, key, level + 1)
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
groups.forEach((group, index) => {
|
|
120
|
+
result.push({
|
|
121
|
+
key: group.key,
|
|
122
|
+
label: group.label,
|
|
123
|
+
fullName: group.label,
|
|
124
|
+
parentKey: null,
|
|
125
|
+
level: 1,
|
|
126
|
+
position: index + Number(showHead) + 1,
|
|
127
|
+
siblingCount: rootCount,
|
|
128
|
+
expandable: true
|
|
129
|
+
})
|
|
130
|
+
if (expanded.has(group.key)) append(group.nodes, group.key, group.key, 2)
|
|
131
|
+
})
|
|
132
|
+
return result
|
|
133
|
+
}, [branches, expanded, search, t])
|
|
134
|
+
|
|
135
|
+
const activeRow =
|
|
136
|
+
rows.find((row) => row.key === activeKey) ??
|
|
137
|
+
rows.find((row) => row.key === value) ??
|
|
138
|
+
rows.find((row) => !row.expandable) ??
|
|
139
|
+
rows[0]
|
|
140
|
+
|
|
141
|
+
const changeOpen = (next: boolean): void => {
|
|
142
|
+
if (next) {
|
|
143
|
+
setQuery('')
|
|
144
|
+
setActiveKey(value)
|
|
145
|
+
setExpanded((previous) => new Set([...previous, ...branchAncestors(value)]))
|
|
146
|
+
}
|
|
147
|
+
setOpen(next)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const changeSearch = (text: string): void => {
|
|
151
|
+
setQuery(text)
|
|
152
|
+
setActiveKey('')
|
|
153
|
+
const filter = text.trim().toLocaleLowerCase()
|
|
154
|
+
if (!filter) return
|
|
155
|
+
const keys = [
|
|
156
|
+
...(branches?.local ?? [])
|
|
157
|
+
.filter((name) => name.toLocaleLowerCase().includes(filter))
|
|
158
|
+
.map((name) => `refs/heads/${name}`),
|
|
159
|
+
...(branches?.remote ?? [])
|
|
160
|
+
.filter((name) => name.toLocaleLowerCase().includes(filter))
|
|
161
|
+
.map((name) => `refs/remotes/${name}`)
|
|
162
|
+
]
|
|
163
|
+
setExpanded((previous) => new Set([...previous, ...keys.flatMap(branchAncestors)]))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const focusRow = (row: BranchRow | undefined): void => {
|
|
167
|
+
if (!row) return
|
|
168
|
+
setActiveKey(row.key)
|
|
169
|
+
const element = rowRefs.current.get(row.key)
|
|
170
|
+
element?.focus({ preventScroll: true })
|
|
171
|
+
element?.scrollIntoView({ block: 'nearest' })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const toggle = (row: BranchRow): void => {
|
|
175
|
+
setExpanded((previous) => {
|
|
176
|
+
const next = new Set(previous)
|
|
177
|
+
if (next.has(row.key)) next.delete(row.key)
|
|
178
|
+
else next.add(row.key)
|
|
179
|
+
return next
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const handleTreeKey = (event: KeyboardEvent<HTMLButtonElement>, row: BranchRow): void => {
|
|
184
|
+
if (event.nativeEvent.isComposing || event.altKey || event.ctrlKey || event.metaKey) return
|
|
185
|
+
const index = rows.indexOf(row)
|
|
186
|
+
switch (event.key) {
|
|
187
|
+
case 'ArrowDown':
|
|
188
|
+
focusRow(rows[Math.min(index + 1, rows.length - 1)])
|
|
189
|
+
break
|
|
190
|
+
case 'ArrowUp':
|
|
191
|
+
focusRow(rows[Math.max(index - 1, 0)])
|
|
192
|
+
break
|
|
193
|
+
case 'Home':
|
|
194
|
+
focusRow(rows[0])
|
|
195
|
+
break
|
|
196
|
+
case 'End':
|
|
197
|
+
focusRow(rows.at(-1))
|
|
198
|
+
break
|
|
199
|
+
case 'ArrowRight':
|
|
200
|
+
if (row.expandable) {
|
|
201
|
+
if (expanded.has(row.key)) focusRow(rows[index + 1])
|
|
202
|
+
else toggle(row)
|
|
203
|
+
}
|
|
204
|
+
break
|
|
205
|
+
case 'ArrowLeft':
|
|
206
|
+
if (row.expandable && expanded.has(row.key)) toggle(row)
|
|
207
|
+
else focusRow(rows.find((candidate) => candidate.key === row.parentKey))
|
|
208
|
+
break
|
|
209
|
+
default:
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
event.preventDefault()
|
|
213
|
+
event.stopPropagation()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<Popover
|
|
218
|
+
open={open}
|
|
219
|
+
onOpenChange={changeOpen}
|
|
220
|
+
onOpenChangeComplete={(next) => {
|
|
221
|
+
if (next && document.activeElement === searchRef.current) {
|
|
222
|
+
rowRefs.current.get(value)?.scrollIntoView({ block: 'nearest' })
|
|
223
|
+
}
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
<PopoverTrigger
|
|
227
|
+
render={
|
|
228
|
+
<Button
|
|
229
|
+
ref={triggerRef}
|
|
230
|
+
type="button"
|
|
231
|
+
variant="outline"
|
|
232
|
+
role="combobox"
|
|
233
|
+
aria-haspopup="dialog"
|
|
234
|
+
aria-label={ariaLabel}
|
|
235
|
+
aria-expanded={open}
|
|
236
|
+
title={selectedLabel || placeholder}
|
|
237
|
+
className={cn(
|
|
238
|
+
'w-full min-w-0 justify-between border-input px-2.5 font-normal focus-visible:-outline-offset-2',
|
|
239
|
+
className
|
|
240
|
+
)}
|
|
241
|
+
>
|
|
242
|
+
<span className={cn('min-w-0 truncate', !value && 'text-muted-foreground')}>
|
|
243
|
+
{selectedLabel || placeholder || common('selectPlaceholder')}
|
|
244
|
+
</span>
|
|
245
|
+
<ChevronsUpDownIcon className="text-muted-foreground" />
|
|
246
|
+
</Button>
|
|
247
|
+
}
|
|
248
|
+
/>
|
|
249
|
+
<PopoverContent
|
|
250
|
+
align="start"
|
|
251
|
+
aria-label={ariaLabel}
|
|
252
|
+
initialFocus={searchRef}
|
|
253
|
+
finalFocus={triggerRef}
|
|
254
|
+
positionerClassName="z-[120]"
|
|
255
|
+
className="max-h-[min(30rem,var(--available-height))] w-[max(26rem,calc(var(--anchor-width)+8rem))] max-w-[min(calc(100vw-2rem),var(--available-width))] gap-0 overflow-hidden p-0"
|
|
256
|
+
>
|
|
257
|
+
<div className="shrink-0 border-b p-2">
|
|
258
|
+
<Input
|
|
259
|
+
ref={searchRef}
|
|
260
|
+
type="search"
|
|
261
|
+
aria-label={common('searchPlaceholder')}
|
|
262
|
+
aria-controls={treeId}
|
|
263
|
+
placeholder={common('searchPlaceholder')}
|
|
264
|
+
value={query}
|
|
265
|
+
onChange={(event) => changeSearch(event.target.value)}
|
|
266
|
+
onKeyDown={(event) => {
|
|
267
|
+
if (event.nativeEvent.isComposing || event.altKey || event.ctrlKey || event.metaKey)
|
|
268
|
+
return
|
|
269
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return
|
|
270
|
+
event.preventDefault()
|
|
271
|
+
event.stopPropagation()
|
|
272
|
+
focusRow(event.key === 'ArrowDown' ? activeRow : rows.at(-1))
|
|
273
|
+
}}
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
<div
|
|
277
|
+
id={treeId}
|
|
278
|
+
role="tree"
|
|
279
|
+
aria-label={ariaLabel}
|
|
280
|
+
className="min-h-0 overflow-y-auto overscroll-contain p-1"
|
|
281
|
+
>
|
|
282
|
+
{rows.map((row) => {
|
|
283
|
+
const selected = !row.expandable && row.key === value
|
|
284
|
+
const expandedRow = row.expandable ? expanded.has(row.key) : undefined
|
|
285
|
+
return (
|
|
286
|
+
<button
|
|
287
|
+
key={row.key}
|
|
288
|
+
ref={(element) => {
|
|
289
|
+
if (element) rowRefs.current.set(row.key, element)
|
|
290
|
+
else rowRefs.current.delete(row.key)
|
|
291
|
+
}}
|
|
292
|
+
type="button"
|
|
293
|
+
role="treeitem"
|
|
294
|
+
aria-label={row.fullName}
|
|
295
|
+
aria-level={row.level}
|
|
296
|
+
aria-posinset={row.position}
|
|
297
|
+
aria-setsize={row.siblingCount}
|
|
298
|
+
aria-selected={row.expandable ? undefined : selected}
|
|
299
|
+
aria-expanded={expandedRow}
|
|
300
|
+
tabIndex={activeRow?.key === row.key ? 0 : -1}
|
|
301
|
+
title={row.fullName}
|
|
302
|
+
className={cn(
|
|
303
|
+
'flex min-h-8 w-full items-center gap-1.5 rounded-md py-1 pr-2 text-left text-sm outline-none transition-colors duration-150 hover:bg-accent focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring [@media(pointer:coarse)]:min-h-10',
|
|
304
|
+
selected && 'bg-accent text-accent-foreground',
|
|
305
|
+
row.level === 1 && row.expandable && 'font-medium text-muted-foreground'
|
|
306
|
+
)}
|
|
307
|
+
style={{ paddingLeft: 8 + Math.min(row.level - 1, 8) * 12 }}
|
|
308
|
+
onFocus={() => setActiveKey(row.key)}
|
|
309
|
+
onKeyDown={(event) => handleTreeKey(event, row)}
|
|
310
|
+
onClick={() => {
|
|
311
|
+
setActiveKey(row.key)
|
|
312
|
+
if (row.expandable) toggle(row)
|
|
313
|
+
else {
|
|
314
|
+
onChange(row.key)
|
|
315
|
+
setOpen(false)
|
|
316
|
+
}
|
|
317
|
+
}}
|
|
318
|
+
>
|
|
319
|
+
{row.expandable ? (
|
|
320
|
+
expandedRow ? (
|
|
321
|
+
<ChevronDownIcon aria-hidden="true" className="size-3.5 shrink-0" />
|
|
322
|
+
) : (
|
|
323
|
+
<ChevronRightIcon aria-hidden="true" className="size-3.5 shrink-0" />
|
|
324
|
+
)
|
|
325
|
+
) : (
|
|
326
|
+
<span aria-hidden="true" className="size-3.5 shrink-0" />
|
|
327
|
+
)}
|
|
328
|
+
<span className="min-w-0 flex-1 break-all">{row.label}</span>
|
|
329
|
+
{selected ? <CheckIcon aria-hidden="true" className="size-3.5 shrink-0" /> : null}
|
|
330
|
+
</button>
|
|
331
|
+
)
|
|
332
|
+
})}
|
|
333
|
+
</div>
|
|
334
|
+
{!rows.length ? (
|
|
335
|
+
<p role="status" className="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
336
|
+
{common('noMatches')}
|
|
337
|
+
</p>
|
|
338
|
+
) : null}
|
|
339
|
+
{activeRow && activeRow.fullName !== activeRow.label ? (
|
|
340
|
+
<div className="max-h-24 shrink-0 overflow-y-auto border-t px-3 py-2 text-xs break-all text-muted-foreground">
|
|
341
|
+
{activeRow.fullName}
|
|
342
|
+
</div>
|
|
343
|
+
) : null}
|
|
344
|
+
</PopoverContent>
|
|
345
|
+
</Popover>
|
|
346
|
+
)
|
|
347
|
+
}
|