@jetcrab/pi-desk 1.1.8 → 1.1.10
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 +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/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-9f478658c50efdbe.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 → ZYtCj4W63FjXCrTLTDbvO}/_buildManifest.js +0 -0
- /package/temp/build/pi-desk/release/.next/static/{ZYYO9lnf3gpocPCjcjYmq → ZYtCj4W63FjXCrTLTDbvO}/_ssgManifest.js +0 -0
package/docs/plugins.md
CHANGED
|
@@ -67,6 +67,18 @@ plugin.push('changed', {})
|
|
|
67
67
|
|
|
68
68
|
Node `plugin.setState()` 则整体替换自身全局状态;Browser 通过 `host.globalState.getSnapshot()/subscribe()` 只读。两者不是同一槽位,均不得读写其他插件状态。宿主地区设置由 `host.settings.getSnapshot()/subscribe()` 只读,不另存权威副本。
|
|
69
69
|
|
|
70
|
+
### 会话预览目录
|
|
71
|
+
|
|
72
|
+
Session 插件可为左侧项目文件增加目录,不改变 Pi 会话 CWD:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
plugin.addPreviewDirectory(directory, { default: true })
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`directory` 为服务端绝对目录;省略第二个参数只登记,`default: true` 同时设为默认预览目录,后登记的默认生效。原 CWD 始终可选;目录规范化后去重,最多登记 32 个。重复快照不会覆盖用户的手动选择。登记只在当前会话运行态内,插件在 `session_start`、`session_tree` 从自身数据恢复后重新登记;不要在普通查询时重复请求默认选择。
|
|
79
|
+
|
|
80
|
+
调用不等待回执;非法路径或超限由宿主拒绝并记日志,实际目录内容按需读取。旧宿主或独立 Pi 不处理该登记;兼容旧 SDK 时使用 `plugin.addPreviewDirectory?.(...)`。已有 `files.preview/reveal/readImage` 的相对路径仍以会话 CWD 为基准。
|
|
81
|
+
|
|
70
82
|
## Browser Entry 与懒界面
|
|
71
83
|
|
|
72
84
|
Entry 默认导出普通同步函数即可;在函数返回前完成全部 `registerContribution(kind, name, definition)`。kind 支持 `application`、`composer-panel`、`settings-page`、`session-sidebar-tab`、`message-view`。Entry 只放元数据、loader、轻量页面状态和事件订阅;返回 disposer 清理自有订阅与资源。
|
|
@@ -110,7 +122,9 @@ Method 调用没有 Browser 视图级取消参数;Global handler 的 `context.
|
|
|
110
122
|
Push 可丢失、不持久化、不 ACK、不重放,也不创建聊天消息。Entry ready、连接恢复 ready、视图 mount 时按需要查询 Method 建立权威基线;不要用页面轮询替代已有的宿主状态同步。
|
|
111
123
|
|
|
112
124
|
- 当前页面轻提示:`plugin.notify({ level: 'success', title: '保存成功' })` 或 `host.notify()`,只在当前页面短暂展示,离线可错过。
|
|
113
|
-
- 服务端全局通知:Node `plugin.notifications.publish({ level: 'success', title: '处理完成', event: { name: 'open-result', data: {} } })`;用返回 ID 调用 `update/delete`。Browser Entry 通过 `plugin.notifications.onEvent('open-result', handler)` 处理点击,成功后宿主删除通知。Node 未重启时重连可恢复;Node 重启或插件释放后清空,不持久化。
|
|
125
|
+
- 服务端全局通知:Node `plugin.notifications.publish({ key: 'result:run-1', level: 'success', title: '处理完成', event: { name: 'open-result', data: { runId: 'run-1' } } })`;用返回 ID 调用 `update/delete`。Browser Entry 通过 `plugin.notifications.onEvent('open-result', handler)` 处理点击,成功后宿主删除通知。Node 未重启时重连可恢复;Node 重启或插件释放后清空,不持久化。
|
|
126
|
+
|
|
127
|
+
`publish()` 的 `key` 可选,去除首尾空白后为 1~256 字符,由业务标识事件,例如 `post:${postId}` 或 `build:${runId}`。同一插件、同一 `key` 的现存通知只保留一条:内容相同不广播;内容变化整体更新文字、级别和点击 Event,返回原 ID,保留创建时间与位置。再次发布时省略 `description/event` 会清除旧值;`update()` 仍只修改文字和级别。不传 `key` 每次新增,不按文字自动合并。通知删除或淘汰后同 `key` 可重新创建,不保存已关闭记录;`key` 只存在于服务端通知内存,不随通知传给浏览器。带 `key` 的发布需使用支持通知去重的宿主;旧宿主会拒绝未知字段,不自动降级为新增通知。
|
|
114
128
|
|
|
115
129
|
## 消息展示
|
|
116
130
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jetcrab/pi-desk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -69,13 +69,13 @@
|
|
|
69
69
|
"viewerjs": "^1.11.8",
|
|
70
70
|
"ws": "^8.21.3",
|
|
71
71
|
"zod": "^4.4.3",
|
|
72
|
-
"@jetcrab/pi-desk-sdk": "^1.1.
|
|
72
|
+
"@jetcrab/pi-desk-sdk": "^1.1.2"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@earendil-works/pi-agent-core": "1.0
|
|
76
|
-
"@earendil-works/pi-ai": "1.0
|
|
77
|
-
"@earendil-works/pi-coding-agent": "1.0
|
|
78
|
-
"@earendil-works/pi-tui": "1.0
|
|
75
|
+
"@earendil-works/pi-agent-core": "1.2.0",
|
|
76
|
+
"@earendil-works/pi-ai": "1.2.0",
|
|
77
|
+
"@earendil-works/pi-coding-agent": "1.2.0",
|
|
78
|
+
"@earendil-works/pi-tui": "1.2.0",
|
|
79
79
|
"@tailwindcss/postcss": "^4",
|
|
80
80
|
"@types/node": "^20",
|
|
81
81
|
"@types/react": "^19",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"yaml": "^2.9.0"
|
|
95
95
|
},
|
|
96
96
|
"peerDependencies": {
|
|
97
|
-
"@earendil-works/pi-coding-agent": "
|
|
97
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
98
98
|
},
|
|
99
99
|
"peerDependenciesMeta": {
|
|
100
100
|
"@earendil-works/pi-coding-agent": {
|
|
@@ -11,7 +11,10 @@ export async function loginL2Auth(clientId: string, input: L2AuthLoginRequest):
|
|
|
11
11
|
const parsed = L2AuthLoginRequestSchema.safeParse(input)
|
|
12
12
|
if (!parsed.success) {
|
|
13
13
|
throw new Error(
|
|
14
|
-
l4LocalizedErrorMessage({
|
|
14
|
+
l4LocalizedErrorMessage({
|
|
15
|
+
msg: '登录失败,请检查登录信息后重试。',
|
|
16
|
+
i18n: { key: 'auth:invalidInput' }
|
|
17
|
+
})
|
|
15
18
|
)
|
|
16
19
|
}
|
|
17
20
|
const response = await fetch('/api/auth/login', {
|
|
@@ -5,7 +5,12 @@ import { useTranslation } from 'react-i18next'
|
|
|
5
5
|
import type { L2AuthSettings } from '@common/l2_biz/auth/l2-auth-contract'
|
|
6
6
|
import { useL4AppSocket } from '@client/l4_foundation/realtime/app-socket/l4-app-socket'
|
|
7
7
|
import { useL4ConfirmDialog } from '@client/l4_foundation/ui/l4-confirm-dialog'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
generateL2AuthPassword,
|
|
10
|
+
getL2AuthSettings,
|
|
11
|
+
logoutL2Auth,
|
|
12
|
+
replaceL2AuthSettings
|
|
13
|
+
} from '../l2-auth-settings-biz'
|
|
9
14
|
|
|
10
15
|
interface AuthSettingsState {
|
|
11
16
|
settings: L2AuthSettings | null
|
|
@@ -17,6 +22,7 @@ interface AuthSettingsState {
|
|
|
17
22
|
setPassword: (value: string) => void
|
|
18
23
|
retry: () => void
|
|
19
24
|
save: () => Promise<void>
|
|
25
|
+
generatePassword: () => void
|
|
20
26
|
disable: () => Promise<void>
|
|
21
27
|
logout: () => Promise<void>
|
|
22
28
|
dialog: React.JSX.Element | null
|
|
@@ -51,6 +57,16 @@ export function useL2AuthSettings(): AuthSettingsState {
|
|
|
51
57
|
return () => controller.abort()
|
|
52
58
|
}, [clientId, attempt, t])
|
|
53
59
|
|
|
60
|
+
const generatePassword = (): void => {
|
|
61
|
+
if (pending.current) return
|
|
62
|
+
try {
|
|
63
|
+
setPassword(generateL2AuthPassword())
|
|
64
|
+
setError(null)
|
|
65
|
+
} catch {
|
|
66
|
+
setError(t('generatePasswordFailed'))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
54
70
|
const save = async (): Promise<void> => {
|
|
55
71
|
if (pending.current || !settings) return
|
|
56
72
|
pending.current = true
|
|
@@ -111,6 +127,7 @@ export function useL2AuthSettings(): AuthSettingsState {
|
|
|
111
127
|
password,
|
|
112
128
|
setPassword,
|
|
113
129
|
save,
|
|
130
|
+
generatePassword,
|
|
114
131
|
disable,
|
|
115
132
|
logout,
|
|
116
133
|
dialog,
|
|
@@ -47,6 +47,23 @@ export async function logoutL2Auth(clientId: string): Promise<void> {
|
|
|
47
47
|
window.location.replace('/login')
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export function generateL2AuthPassword(): string {
|
|
51
|
+
// 64 个字符可直接映射随机字节,不产生取模偏差。
|
|
52
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@'
|
|
53
|
+
while (true) {
|
|
54
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16))
|
|
55
|
+
const password = Array.from(bytes, (value) => alphabet[value % 64]).join('')
|
|
56
|
+
if (
|
|
57
|
+
/[A-Z]/.test(password) &&
|
|
58
|
+
/[a-z]/.test(password) &&
|
|
59
|
+
/[0-9]/.test(password) &&
|
|
60
|
+
/[!@]/.test(password)
|
|
61
|
+
) {
|
|
62
|
+
return password
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
export async function replaceL2AuthSettings(
|
|
51
68
|
clientId: string,
|
|
52
69
|
input: L2AuthSettingsReplaceRequest
|
|
@@ -55,7 +72,7 @@ export async function replaceL2AuthSettings(
|
|
|
55
72
|
if (!parsed.success) {
|
|
56
73
|
throw new Error(
|
|
57
74
|
l4LocalizedErrorMessage({
|
|
58
|
-
msg: '
|
|
75
|
+
msg: '密码不符合要求,请按输入框下方的规则调整。',
|
|
59
76
|
i18n: { key: 'auth:settingsInvalid' }
|
|
60
77
|
})
|
|
61
78
|
)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
ChevronRightIcon,
|
|
5
|
+
DicesIcon,
|
|
5
6
|
LoaderCircleIcon,
|
|
6
7
|
LogOutIcon,
|
|
7
8
|
ShieldCheckIcon,
|
|
@@ -76,15 +77,28 @@ export function L2AuthSettings(): React.JSX.Element {
|
|
|
76
77
|
</div>
|
|
77
78
|
<div className="min-w-0 max-w-[40rem] space-y-4">
|
|
78
79
|
<div className="space-y-1">
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
81
|
+
<label className="block text-sm font-medium" htmlFor="auth-password">
|
|
82
|
+
{t(enabled ? 'newPassword' : 'password')}
|
|
83
|
+
</label>
|
|
84
|
+
<Button
|
|
85
|
+
type="button"
|
|
86
|
+
variant="ghost"
|
|
87
|
+
size="sm"
|
|
88
|
+
disabled={busy}
|
|
89
|
+
title={t('generatePasswordHint')}
|
|
90
|
+
onClick={state.generatePassword}
|
|
91
|
+
>
|
|
92
|
+
<DicesIcon aria-hidden="true" />
|
|
93
|
+
{t('generatePassword')}
|
|
94
|
+
</Button>
|
|
95
|
+
</div>
|
|
82
96
|
<L4PasswordInput
|
|
83
97
|
id="auth-password"
|
|
84
98
|
name="password"
|
|
85
99
|
className="[&::-ms-reveal]:hidden"
|
|
86
100
|
autoComplete="new-password"
|
|
87
|
-
minLength={
|
|
101
|
+
minLength={12}
|
|
88
102
|
maxLength={256}
|
|
89
103
|
required
|
|
90
104
|
disabled={busy}
|
|
@@ -92,7 +106,7 @@ export function L2AuthSettings(): React.JSX.Element {
|
|
|
92
106
|
aria-describedby={`auth-password-hint${state.error ? ' auth-settings-error' : ''}`}
|
|
93
107
|
onChange={(event) => state.setPassword(event.target.value)}
|
|
94
108
|
/>
|
|
95
|
-
<p id="auth-password-hint" className="text-
|
|
109
|
+
<p id="auth-password-hint" className="text-sm text-muted-foreground">
|
|
96
110
|
{t('passwordHint')}
|
|
97
111
|
</p>
|
|
98
112
|
</div>
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { L2WorkSessionListItem } from '@common/l2_biz/work-session/l2-work-session-contract'
|
|
2
|
+
import type { L2ChatSource } from '@common/l2_biz/chat/l2-chat-contract'
|
|
3
|
+
import {
|
|
4
|
+
projectPreviewDirectoryKey,
|
|
5
|
+
type L4ProjectPreviewState
|
|
6
|
+
} from '@common/l4_foundation/file/l4-project-preview-contract'
|
|
2
7
|
import type { L4CodePreviewState } from '@client/l4_foundation/ui/code/l4-code-preview'
|
|
3
8
|
import { readL3ImagePreviewMode } from '@client/l3_modules/preferences/l3-work-session-file-preferences'
|
|
4
9
|
import {
|
|
@@ -26,12 +31,21 @@ export interface L2SessionFileWorkspace extends L3ProjectFileState {
|
|
|
26
31
|
readonly activePane: 'chat' | 'files'
|
|
27
32
|
}
|
|
28
33
|
type SourceRef = Pick<L2WorkSessionListItem, 'workId' | 'cwd' | 'sessionId' | 'branchId'>
|
|
34
|
+
interface FileRoot {
|
|
35
|
+
files: L3ProjectFileRuntime
|
|
36
|
+
unsubscribe: () => void
|
|
37
|
+
}
|
|
38
|
+
interface SessionDirectories {
|
|
39
|
+
directories: string[]
|
|
40
|
+
defaultDirectory: string | null
|
|
41
|
+
selectedDirectory: string
|
|
42
|
+
}
|
|
29
43
|
interface SessionLayout {
|
|
30
44
|
files: L3ProjectFileRuntime
|
|
45
|
+
roots: Map<string, FileRoot>
|
|
31
46
|
windowOpen: boolean
|
|
32
47
|
chatWidthPercent: number
|
|
33
48
|
activePane: 'chat' | 'files'
|
|
34
|
-
unsubscribe: () => void
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
export class L2FileWorkspaceRuntime {
|
|
@@ -39,6 +53,7 @@ export class L2FileWorkspaceRuntime {
|
|
|
39
53
|
private readonly listeners = new Set<() => void>()
|
|
40
54
|
private readonly workspaces = new Map<string, SessionLayout>()
|
|
41
55
|
private readonly sources = new Map<string, SourceRef>()
|
|
56
|
+
private readonly previewDirectories = new Map<string, SessionDirectories>()
|
|
42
57
|
private expandedWorkId: string | null = null
|
|
43
58
|
private revision = 0
|
|
44
59
|
constructor(
|
|
@@ -69,6 +84,69 @@ export class L2FileWorkspaceRuntime {
|
|
|
69
84
|
getExpandedWorkId(): string | null {
|
|
70
85
|
return this.expandedWorkId
|
|
71
86
|
}
|
|
87
|
+
getDirectories(workId: string): readonly string[] {
|
|
88
|
+
return this.previewDirectories.get(workId)?.directories ?? []
|
|
89
|
+
}
|
|
90
|
+
getDirectory(workId: string): string {
|
|
91
|
+
const selection = this.previewDirectories.get(workId)
|
|
92
|
+
if (!selection) throw new Error(l2WorkbenchText('sourceChanged'))
|
|
93
|
+
return selection.selectedDirectory
|
|
94
|
+
}
|
|
95
|
+
applyPreviewDirectories(source: L2ChatSource, state: L4ProjectPreviewState): void {
|
|
96
|
+
const current = this.sources.get(source.workId)
|
|
97
|
+
if (!current || current.sessionId !== source.sessionId || current.branchId !== source.branchId)
|
|
98
|
+
return
|
|
99
|
+
const previous = this.previewDirectories.get(source.workId)!
|
|
100
|
+
const unique = new Map([[projectPreviewDirectoryKey(current.cwd), current.cwd]])
|
|
101
|
+
for (const directory of state.directories) {
|
|
102
|
+
const key = projectPreviewDirectoryKey(directory)
|
|
103
|
+
if (!unique.has(key)) unique.set(key, directory)
|
|
104
|
+
}
|
|
105
|
+
const defaultDirectory = state.defaultDirectory
|
|
106
|
+
? (unique.get(projectPreviewDirectoryKey(state.defaultDirectory)) ?? null)
|
|
107
|
+
: null
|
|
108
|
+
const previousDefaultKey = previous.defaultDirectory
|
|
109
|
+
? projectPreviewDirectoryKey(previous.defaultDirectory)
|
|
110
|
+
: null
|
|
111
|
+
const defaultKey = defaultDirectory ? projectPreviewDirectoryKey(defaultDirectory) : null
|
|
112
|
+
const selectedDirectory =
|
|
113
|
+
previousDefaultKey !== defaultKey
|
|
114
|
+
? (defaultDirectory ?? current.cwd)
|
|
115
|
+
: (unique.get(projectPreviewDirectoryKey(previous.selectedDirectory)) ?? current.cwd)
|
|
116
|
+
const directories = [...unique.values()]
|
|
117
|
+
if (
|
|
118
|
+
previous.defaultDirectory === defaultDirectory &&
|
|
119
|
+
previous.selectedDirectory === selectedDirectory &&
|
|
120
|
+
previous.directories.length === directories.length &&
|
|
121
|
+
previous.directories.every((directory, index) => directory === directories[index])
|
|
122
|
+
)
|
|
123
|
+
return
|
|
124
|
+
this.previewDirectories.set(source.workId, { directories, defaultDirectory, selectedDirectory })
|
|
125
|
+
const layout = this.workspaces.get(source.workId)
|
|
126
|
+
if (layout) {
|
|
127
|
+
for (const [key, root] of layout.roots) {
|
|
128
|
+
if (unique.has(key)) continue
|
|
129
|
+
root.unsubscribe()
|
|
130
|
+
root.files.dispose()
|
|
131
|
+
layout.roots.delete(key)
|
|
132
|
+
}
|
|
133
|
+
this.switchFiles(source.workId, layout)
|
|
134
|
+
}
|
|
135
|
+
this.publish()
|
|
136
|
+
}
|
|
137
|
+
selectDirectory(workId: string, directory: string): void {
|
|
138
|
+
const selection = this.previewDirectories.get(workId)
|
|
139
|
+
if (!selection) throw new Error(l2WorkbenchText('sourceChanged'))
|
|
140
|
+
const registered = selection.directories.find(
|
|
141
|
+
(candidate) => projectPreviewDirectoryKey(candidate) === projectPreviewDirectoryKey(directory)
|
|
142
|
+
)
|
|
143
|
+
if (!registered) throw new Error(l2WorkbenchText('sourceChanged'))
|
|
144
|
+
if (selection.selectedDirectory === registered) return
|
|
145
|
+
selection.selectedDirectory = registered
|
|
146
|
+
const layout = this.workspaces.get(workId)
|
|
147
|
+
if (layout) this.switchFiles(workId, layout)
|
|
148
|
+
this.publish()
|
|
149
|
+
}
|
|
72
150
|
reconcileWorkSessions(sessions: readonly L2WorkSessionListItem[]): void {
|
|
73
151
|
this.standaloneImages.reconcileWorkSessions(sessions)
|
|
74
152
|
const next = new Map(
|
|
@@ -88,15 +166,24 @@ export class L2FileWorkspaceRuntime {
|
|
|
88
166
|
continue
|
|
89
167
|
const layout = this.workspaces.get(id)
|
|
90
168
|
if (layout) {
|
|
91
|
-
|
|
92
|
-
layout.files.dispose()
|
|
169
|
+
this.disposeLayout(layout)
|
|
93
170
|
this.workspaces.delete(id)
|
|
94
|
-
changed = true
|
|
95
171
|
}
|
|
172
|
+
this.previewDirectories.delete(id)
|
|
173
|
+
changed = true
|
|
96
174
|
if (this.expandedWorkId === id) this.expandedWorkId = null
|
|
97
175
|
}
|
|
98
176
|
this.sources.clear()
|
|
99
|
-
for (const [id, source] of next)
|
|
177
|
+
for (const [id, source] of next) {
|
|
178
|
+
this.sources.set(id, source)
|
|
179
|
+
if (!this.previewDirectories.has(id)) {
|
|
180
|
+
this.previewDirectories.set(id, {
|
|
181
|
+
directories: [source.cwd],
|
|
182
|
+
defaultDirectory: null,
|
|
183
|
+
selectedDirectory: source.cwd
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
}
|
|
100
187
|
if (changed) this.publish()
|
|
101
188
|
}
|
|
102
189
|
ensureRoot(id: string): Promise<void> {
|
|
@@ -214,7 +301,9 @@ export class L2FileWorkspaceRuntime {
|
|
|
214
301
|
}
|
|
215
302
|
handleGitBranchChanged(cwd: string, root: string): void {
|
|
216
303
|
for (const layout of this.workspaces.values()) {
|
|
217
|
-
|
|
304
|
+
for (const entry of layout.roots.values()) {
|
|
305
|
+
if (entry.files.cwd === cwd) entry.files.handleGitBranchChanged(root)
|
|
306
|
+
}
|
|
218
307
|
}
|
|
219
308
|
}
|
|
220
309
|
saveEditorState(id: string, path: string, state: L4CodePreviewState | null): void {
|
|
@@ -228,12 +317,10 @@ export class L2FileWorkspaceRuntime {
|
|
|
228
317
|
}
|
|
229
318
|
dispose(): void {
|
|
230
319
|
this.standaloneImages.dispose()
|
|
231
|
-
for (const layout of this.workspaces.values())
|
|
232
|
-
layout.unsubscribe()
|
|
233
|
-
layout.files.dispose()
|
|
234
|
-
}
|
|
320
|
+
for (const layout of this.workspaces.values()) this.disposeLayout(layout)
|
|
235
321
|
this.workspaces.clear()
|
|
236
322
|
this.sources.clear()
|
|
323
|
+
this.previewDirectories.clear()
|
|
237
324
|
this.expandedWorkId = null
|
|
238
325
|
this.listeners.clear()
|
|
239
326
|
}
|
|
@@ -242,9 +329,21 @@ export class L2FileWorkspaceRuntime {
|
|
|
242
329
|
if (!source) throw new Error(l2WorkbenchText('sourceChanged'))
|
|
243
330
|
const existing = this.workspaces.get(id)
|
|
244
331
|
if (existing) return existing
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
332
|
+
const layout: SessionLayout = {
|
|
333
|
+
files: this.createFiles(id, this.getDirectory(id)),
|
|
334
|
+
roots: new Map(),
|
|
335
|
+
windowOpen: false,
|
|
336
|
+
chatWidthPercent: 50,
|
|
337
|
+
activePane: 'files'
|
|
338
|
+
}
|
|
339
|
+
this.attachRoot(id, layout, layout.files)
|
|
340
|
+
this.workspaces.set(id, layout)
|
|
341
|
+
return layout
|
|
342
|
+
}
|
|
343
|
+
private createFiles(id: string, cwd: string): L3ProjectFileRuntime {
|
|
344
|
+
const context = { workId: id, cwd }
|
|
345
|
+
return new L3ProjectFileRuntime(
|
|
346
|
+
cwd,
|
|
248
347
|
{
|
|
249
348
|
list: (path, signal) => this.biz.list({ ...context, path }, signal),
|
|
250
349
|
search: (query, signal) => this.biz.search({ ...context, query }, signal),
|
|
@@ -266,22 +365,44 @@ export class L2FileWorkspaceRuntime {
|
|
|
266
365
|
}
|
|
267
366
|
}
|
|
268
367
|
)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
activePane: 'files',
|
|
274
|
-
unsubscribe: () => undefined
|
|
275
|
-
}
|
|
276
|
-
layout.unsubscribe = files.subscribe(() => {
|
|
368
|
+
}
|
|
369
|
+
private attachRoot(id: string, layout: SessionLayout, files: L3ProjectFileRuntime): void {
|
|
370
|
+
const unsubscribe = files.subscribe(() => {
|
|
371
|
+
if (layout.files !== files) return
|
|
277
372
|
if (!files.state.activePath && !files.state.activeDiff && this.expandedWorkId !== id) {
|
|
278
373
|
layout.windowOpen = false
|
|
279
374
|
layout.activePane = 'chat'
|
|
280
375
|
}
|
|
281
376
|
this.publish()
|
|
282
377
|
})
|
|
283
|
-
|
|
284
|
-
|
|
378
|
+
layout.roots.set(projectPreviewDirectoryKey(files.cwd), { files, unsubscribe })
|
|
379
|
+
}
|
|
380
|
+
private switchFiles(id: string, layout: SessionLayout): void {
|
|
381
|
+
const directory = this.getDirectory(id)
|
|
382
|
+
const key = projectPreviewDirectoryKey(directory)
|
|
383
|
+
if (projectPreviewDirectoryKey(layout.files.cwd) === key) return
|
|
384
|
+
let root = layout.roots.get(key)
|
|
385
|
+
if (!root) {
|
|
386
|
+
const files = this.createFiles(id, directory)
|
|
387
|
+
this.attachRoot(id, layout, files)
|
|
388
|
+
root = layout.roots.get(key)!
|
|
389
|
+
}
|
|
390
|
+
layout.files = root.files
|
|
391
|
+
if (
|
|
392
|
+
!root.files.state.activePath &&
|
|
393
|
+
!root.files.state.activeDiff &&
|
|
394
|
+
this.expandedWorkId !== id
|
|
395
|
+
) {
|
|
396
|
+
layout.windowOpen = false
|
|
397
|
+
layout.activePane = 'chat'
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
private disposeLayout(layout: SessionLayout): void {
|
|
401
|
+
for (const root of layout.roots.values()) {
|
|
402
|
+
root.unsubscribe()
|
|
403
|
+
root.files.dispose()
|
|
404
|
+
}
|
|
405
|
+
layout.roots.clear()
|
|
285
406
|
}
|
|
286
407
|
private publish(): void {
|
|
287
408
|
this.revision += 1
|
|
@@ -53,8 +53,11 @@ export function L2FileWorkspaceView({
|
|
|
53
53
|
}: L2FileWorkspaceViewProps): React.JSX.Element | null {
|
|
54
54
|
const { t } = useTranslation('workbench')
|
|
55
55
|
useSyncExternalStore(runtime.subscribe, runtime.getRevision, runtime.getRevision)
|
|
56
|
-
const { workId,
|
|
56
|
+
const { workId, projectName } = workSession
|
|
57
|
+
const cwd = runtime.getDirectory(workId)
|
|
57
58
|
const workspace = runtime.getWorkspace(workId)
|
|
59
|
+
const directoryName = cwd.replaceAll('\\', '/').split('/').filter(Boolean).at(-1) ?? cwd
|
|
60
|
+
const previewProjectName = cwd === workSession.cwd ? projectName : directoryName
|
|
58
61
|
const [directoryOpen, setDirectoryOpen] = useState(true)
|
|
59
62
|
const documentRef = useRef<L3ProjectDocumentHandle>(null)
|
|
60
63
|
const expanded = presentation === 'expanded'
|
|
@@ -137,9 +140,9 @@ export function L2FileWorkspaceView({
|
|
|
137
140
|
</p>
|
|
138
141
|
<p
|
|
139
142
|
className="truncate text-xs text-muted-foreground"
|
|
140
|
-
title={`${
|
|
143
|
+
title={`${cwd} · ${diffPath ?? activePath ?? ''}`}
|
|
141
144
|
>
|
|
142
|
-
{
|
|
145
|
+
{previewProjectName}
|
|
143
146
|
{(diffPath ?? activePath) ? ` · ${diffPath ?? activePath}` : ''}
|
|
144
147
|
</p>
|
|
145
148
|
</div>
|
|
@@ -178,7 +181,7 @@ export function L2FileWorkspaceView({
|
|
|
178
181
|
size="icon-sm"
|
|
179
182
|
aria-label={t('newTerminal')}
|
|
180
183
|
title={t('newTerminal')}
|
|
181
|
-
onClick={() => onOpenTerminal(cwd)}
|
|
184
|
+
onClick={() => onOpenTerminal(workSession.cwd)}
|
|
182
185
|
>
|
|
183
186
|
<TerminalIcon className="size-4" />
|
|
184
187
|
</Button>
|
|
@@ -197,7 +200,7 @@ export function L2FileWorkspaceView({
|
|
|
197
200
|
</div>
|
|
198
201
|
<L2ProjectFileBrowser
|
|
199
202
|
workId={workId}
|
|
200
|
-
cwd={cwd}
|
|
203
|
+
cwd={workSession.cwd}
|
|
201
204
|
projectName={projectName}
|
|
202
205
|
runtime={runtime}
|
|
203
206
|
gitRuntime={gitRuntime}
|
|
@@ -217,6 +220,7 @@ export function L2FileWorkspaceView({
|
|
|
217
220
|
onClose={(path, scope) => runtime.closeTabs(workId, path, scope)}
|
|
218
221
|
/>
|
|
219
222
|
<L3ProjectDocumentView
|
|
223
|
+
key={cwd}
|
|
220
224
|
ref={documentRef}
|
|
221
225
|
runtime={runtime.getFileRuntime(workId)}
|
|
222
226
|
gitRuntime={gitRuntime}
|
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { ChevronDownIcon, FolderIcon } from 'lucide-react'
|
|
4
|
+
import { useSyncExternalStore } from 'react'
|
|
5
|
+
import { useTranslation } from 'react-i18next'
|
|
6
|
+
import { projectPreviewDirectoryKey } from '@common/l4_foundation/file/l4-project-preview-contract'
|
|
7
|
+
import {
|
|
8
|
+
DropdownMenu,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
DropdownMenuContent,
|
|
11
|
+
DropdownMenuGroup,
|
|
12
|
+
DropdownMenuLabel,
|
|
13
|
+
DropdownMenuRadioGroup,
|
|
14
|
+
DropdownMenuRadioItem
|
|
15
|
+
} from '@client/l4_foundation/ui/shadcn/dropdown-menu'
|
|
3
16
|
import { L3ProjectFileBrowser } from '@client/l3_modules/project-files/l3-project-file-browser'
|
|
4
17
|
import type { L2FileWorkspaceRuntime } from './l2-file-workspace-runtime'
|
|
5
18
|
import type { L2GitWorkspaceRuntime } from '../git-workspace/l2-git-workspace-runtime'
|
|
6
19
|
import type { L2GitBranchPickerTarget } from '../git-workspace/l2-git-workspace-model'
|
|
7
20
|
|
|
21
|
+
function directoryName(directory: string): string {
|
|
22
|
+
return directory.replaceAll('\\', '/').split('/').filter(Boolean).at(-1) ?? directory
|
|
23
|
+
}
|
|
24
|
+
function compactDirectory(directory: string): string {
|
|
25
|
+
const parts = directory.replaceAll('\\', '/').split('/').filter(Boolean)
|
|
26
|
+
return parts.length <= 2 ? directory : `…/${parts.slice(-2).join('/')}`
|
|
27
|
+
}
|
|
28
|
+
|
|
8
29
|
interface Props {
|
|
9
30
|
workId: string
|
|
10
31
|
cwd: string
|
|
@@ -23,15 +44,83 @@ export function L2ProjectFileBrowser({
|
|
|
23
44
|
onOpenGitBranches,
|
|
24
45
|
onFileOpened
|
|
25
46
|
}: Props): React.JSX.Element {
|
|
47
|
+
const { t } = useTranslation('projectFiles')
|
|
48
|
+
useSyncExternalStore(runtime.subscribe, runtime.getRevision, runtime.getRevision)
|
|
49
|
+
const directory = runtime.getDirectory(workId)
|
|
50
|
+
const directories = runtime.getDirectories(workId)
|
|
51
|
+
const original = projectPreviewDirectoryKey(cwd) === projectPreviewDirectoryKey(directory)
|
|
52
|
+
const name = original ? projectName : directoryName(directory)
|
|
26
53
|
return (
|
|
27
54
|
<L3ProjectFileBrowser
|
|
28
|
-
cwd={
|
|
29
|
-
projectName={
|
|
55
|
+
cwd={directory}
|
|
56
|
+
projectName={name}
|
|
57
|
+
gitReadOnly={!original}
|
|
58
|
+
rootTitle={
|
|
59
|
+
directories.length > 1 ? (
|
|
60
|
+
<DropdownMenu>
|
|
61
|
+
<DropdownMenuTrigger
|
|
62
|
+
data-testid="project-preview-directory-trigger"
|
|
63
|
+
aria-label={t('selectPreviewDirectory')}
|
|
64
|
+
title={directory}
|
|
65
|
+
className="flex min-h-7 min-w-0 flex-1 items-center gap-1 rounded-md px-1 text-left hover:bg-accent focus-visible:outline-2 focus-visible:outline-ring"
|
|
66
|
+
>
|
|
67
|
+
<span className="max-w-[45%] truncate text-xs font-semibold">{name}</span>
|
|
68
|
+
<span className="text-xs text-muted-foreground">·</span>
|
|
69
|
+
<span className="min-w-0 flex-1 truncate font-mono text-xs text-muted-foreground">
|
|
70
|
+
{compactDirectory(directory)}
|
|
71
|
+
</span>
|
|
72
|
+
<ChevronDownIcon className="size-3.5 shrink-0 text-muted-foreground" />
|
|
73
|
+
</DropdownMenuTrigger>
|
|
74
|
+
<DropdownMenuContent
|
|
75
|
+
finalFocus
|
|
76
|
+
className="w-[23rem] min-w-0 max-w-[calc(100vw-1.5rem)]"
|
|
77
|
+
aria-label={t('previewDirectories')}
|
|
78
|
+
>
|
|
79
|
+
<DropdownMenuGroup>
|
|
80
|
+
<DropdownMenuLabel>{t('previewDirectories')}</DropdownMenuLabel>
|
|
81
|
+
<DropdownMenuRadioGroup
|
|
82
|
+
value={directory}
|
|
83
|
+
onValueChange={(value) => runtime.selectDirectory(workId, value)}
|
|
84
|
+
>
|
|
85
|
+
{directories.map((root) => (
|
|
86
|
+
<DropdownMenuRadioItem
|
|
87
|
+
key={projectPreviewDirectoryKey(root)}
|
|
88
|
+
value={root}
|
|
89
|
+
data-testid="project-preview-directory-option"
|
|
90
|
+
data-directory={root}
|
|
91
|
+
className="items-start py-2 data-checked:bg-muted"
|
|
92
|
+
>
|
|
93
|
+
<FolderIcon className="mt-0.5 size-4 text-muted-foreground" />
|
|
94
|
+
<span className="min-w-0 flex-1">
|
|
95
|
+
<span className="flex items-start gap-2">
|
|
96
|
+
<span className="min-w-0 flex-1 break-all">
|
|
97
|
+
{projectPreviewDirectoryKey(root) === projectPreviewDirectoryKey(cwd)
|
|
98
|
+
? projectName
|
|
99
|
+
: directoryName(root)}
|
|
100
|
+
</span>
|
|
101
|
+
{projectPreviewDirectoryKey(root) === projectPreviewDirectoryKey(cwd) ? (
|
|
102
|
+
<span className="shrink-0 text-xs text-muted-foreground">
|
|
103
|
+
{t('sessionDirectory')}
|
|
104
|
+
</span>
|
|
105
|
+
) : null}
|
|
106
|
+
</span>
|
|
107
|
+
<span className="mt-0.5 block break-all font-mono text-xs leading-5 text-muted-foreground">
|
|
108
|
+
{root}
|
|
109
|
+
</span>
|
|
110
|
+
</span>
|
|
111
|
+
</DropdownMenuRadioItem>
|
|
112
|
+
))}
|
|
113
|
+
</DropdownMenuRadioGroup>
|
|
114
|
+
</DropdownMenuGroup>
|
|
115
|
+
</DropdownMenuContent>
|
|
116
|
+
</DropdownMenu>
|
|
117
|
+
) : undefined
|
|
118
|
+
}
|
|
30
119
|
runtime={runtime.getFileRuntime(workId)}
|
|
31
120
|
gitRuntime={gitRuntime}
|
|
32
|
-
onOpenGitBranches={(repositoryRoot) =>
|
|
33
|
-
onOpenGitBranches({ workId, cwd, projectName, repositoryRoot })
|
|
34
|
-
}
|
|
121
|
+
onOpenGitBranches={(repositoryRoot) => {
|
|
122
|
+
if (original) onOpenGitBranches({ workId, cwd, projectName, repositoryRoot })
|
|
123
|
+
}}
|
|
35
124
|
onFileOpened={() => {
|
|
36
125
|
runtime.showWindow(workId)
|
|
37
126
|
onFileOpened?.()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { L2WorkSessionListItem } from '@common/l2_biz/work-session/l2-work-session-contract'
|
|
2
|
+
import {
|
|
3
|
+
L4_PROJECT_PREVIEW_PLUGIN_KEY,
|
|
4
|
+
readL4ProjectPreviewState
|
|
5
|
+
} from '@common/l4_foundation/file/l4-project-preview-contract'
|
|
6
|
+
import type { L2WorkbenchChatRuntime } from '../l2-workbench-chat'
|
|
7
|
+
import type { L2GitWorkspaceRuntime } from '../git-workspace/l2-git-workspace-runtime'
|
|
8
|
+
import type { L2FileWorkspaceRuntime } from './l2-file-workspace-runtime'
|
|
9
|
+
|
|
10
|
+
export function syncL2ProjectPreviewGitReferences(
|
|
11
|
+
files: L2FileWorkspaceRuntime,
|
|
12
|
+
git: L2GitWorkspaceRuntime,
|
|
13
|
+
sessions: readonly L2WorkSessionListItem[],
|
|
14
|
+
focusedWorkId: string | null
|
|
15
|
+
): void {
|
|
16
|
+
git.reconcileWorkSessions(
|
|
17
|
+
sessions,
|
|
18
|
+
focusedWorkId,
|
|
19
|
+
sessions.flatMap(({ workId }) => files.getDirectories(workId).map((cwd) => ({ workId, cwd })))
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function subscribeL2ProjectPreviewDirectories(
|
|
24
|
+
chat: L2WorkbenchChatRuntime,
|
|
25
|
+
files: L2FileWorkspaceRuntime,
|
|
26
|
+
git: L2GitWorkspaceRuntime,
|
|
27
|
+
getSessions: () => readonly L2WorkSessionListItem[],
|
|
28
|
+
getFocusedWorkId: () => string | null
|
|
29
|
+
): () => void {
|
|
30
|
+
const unsubscribeChat = chat.subscribeSourceEvents(({ source, event }, state) => {
|
|
31
|
+
if (event.type !== 'session_sync' && event.type !== 'runtime_update') return
|
|
32
|
+
files.applyPreviewDirectories(
|
|
33
|
+
source,
|
|
34
|
+
readL4ProjectPreviewState(state.runtime.plugins[L4_PROJECT_PREVIEW_PLUGIN_KEY])
|
|
35
|
+
)
|
|
36
|
+
syncL2ProjectPreviewGitReferences(files, git, getSessions(), getFocusedWorkId())
|
|
37
|
+
})
|
|
38
|
+
for (const session of getSessions()) {
|
|
39
|
+
const state = chat.getSourceState(session)
|
|
40
|
+
if (state.syncStatus === 'loading') continue
|
|
41
|
+
files.applyPreviewDirectories(
|
|
42
|
+
session,
|
|
43
|
+
readL4ProjectPreviewState(state.runtime.plugins[L4_PROJECT_PREVIEW_PLUGIN_KEY])
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
syncL2ProjectPreviewGitReferences(files, git, getSessions(), getFocusedWorkId())
|
|
47
|
+
return unsubscribeChat
|
|
48
|
+
}
|