@jetcrab/pi-desk 1.1.6-dev → 1.1.8-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/README.md +4 -4
- package/README.zh-CN.md +4 -4
- package/docs/README.md +8 -0
- package/docs/doc_asset/client-overview.webp +0 -0
- package/docs/doc_asset/multi-window.webp +0 -0
- package/docs/{pi-desk/plugins.md → plugins.md} +108 -1
- package/package.json +2 -2
- package/src/client/l2_biz/project-preview/l2-project-preview-dialog.tsx +3 -0
- package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-view.tsx +8 -0
- package/src/client/l3_modules/project-files/l3-project-document-git.ts +82 -0
- package/src/client/l3_modules/project-files/l3-project-document-view.tsx +102 -5
- package/src/client/l3_modules/project-files/l3-project-file-browser.tsx +2 -6
- package/src/client/l3_modules/project-files/l3-project-file-runtime.ts +83 -1
- package/src/client/l3_modules/project-files/l3-project-file-tabs.tsx +24 -2
- package/src/client/l3_modules/project-files/l3-project-git-model.ts +32 -0
- package/src/client/l4_foundation/ui/code/l4-code-diff.tsx +25 -5
- package/src/client/l4_foundation/ui/code/l4-code-line-changes.ts +49 -0
- package/src/client/l4_foundation/ui/code/l4-code-line-changes.worker.ts +10 -0
- package/src/client/l4_foundation/ui/code/l4-code-line-markers.module.css +60 -0
- package/src/client/l4_foundation/ui/code/l4-code-line-markers.ts +156 -0
- package/src/client/l4_foundation/ui/code/l4-code-preview.tsx +61 -5
- package/src/client/l4_foundation/ui/code/l4-monaco-lines-diff.d.ts +21 -0
- package/src/common/l3_modules/project-files/l3-project-files-locale-messages.ts +16 -0
- package/src/server/l4_foundation/pidesk/l4-pidesk-runtime.ts +1 -1
- 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 +5 -4
- 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.js +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.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 +3 -3
- 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/20842.js +1 -0
- package/temp/build/pi-desk/release/.next/server/chunks/25406.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/{9248.js → 354.js} +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/35985.js +6 -6
- package/temp/build/pi-desk/release/.next/server/chunks/45671.js +1 -0
- package/temp/build/pi-desk/release/.next/server/chunks/47757.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/56284.js +62 -0
- package/temp/build/pi-desk/release/.next/server/chunks/73691.js +1 -0
- package/temp/build/pi-desk/release/.next/server/chunks/82499.js +2 -2
- package/temp/build/pi-desk/release/.next/server/chunks/94296.js +2 -2
- package/temp/build/pi-desk/release/.next/server/chunks/pi-desk-line-changes.js +1 -0
- 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/webpack-runtime.js +1 -1
- package/temp/build/pi-desk/release/.next/static/chunks/28739.540a4f24a8eaa3fd.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/31087-333cae396a52f232.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/3814-35cfab0ce3aa7c41.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/38459.9a9b6f4f3ec3ff50.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/45992-a39a014497ec4dd1.js +13 -0
- package/temp/build/pi-desk/release/.next/static/chunks/53291.ab6ef49016107124.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/{54206.82a63ab971911eda.js → 54206.ffb8c581c6661ae2.js} +2 -2
- package/temp/build/pi-desk/release/.next/static/chunks/5598.f34dce2a9de0d0e3.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/70669.8b4231a8675499e0.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/95287.34b860abf26f64a5.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/layout-586f6f88483051a7.js +3 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/{page-83ed165c9a3835c1.js → page-0f2cf68a4666420b.js} +3 -3
- package/temp/build/pi-desk/release/.next/static/chunks/pi-desk-line-changes.49f8d59c256621d6.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/webpack-26261285e181235a.js +1 -0
- package/temp/build/pi-desk/release/.next/static/css/{f6a76a493aae9ae3.css → adb1d4e48dd43c0a.css} +1 -1
- package/temp/build/pi-desk/release/.next/trace-build +1 -1
- package/docs/pi-desk/README.md +0 -47
- package/temp/build/pi-desk/release/.next/server/chunks/6656.js +0 -1
- package/temp/build/pi-desk/release/.next/server/chunks/89236.js +0 -62
- package/temp/build/pi-desk/release/.next/static/chunks/19991.02fcb7c8ca3b1fe4.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/26285.54b8bcbf7a198c84.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/52354-6461358c522130be.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/74132-876f256c7a6101af.js +0 -13
- package/temp/build/pi-desk/release/.next/static/chunks/79615.7d68b269d93fcb93.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/app/layout-4faf3a52d219296d.js +0 -3
- package/temp/build/pi-desk/release/.next/static/chunks/webpack-7bfbf627379fb4c1.js +0 -1
- /package/docs/{pi-desk/commands.md → commands.md} +0 -0
- /package/docs/{pi-desk/examples → examples}/local-application/browser/application.js +0 -0
- /package/docs/{pi-desk/examples → examples}/local-application/browser/entry.js +0 -0
- /package/docs/{pi-desk/examples → examples}/local-application/package.json +0 -0
- /package/docs/{pi-desk/examples → examples}/local-application/pi-desk.ts +0 -0
- /package/temp/build/pi-desk/release/.next/static/{NwX_o5zDfv-NABRLmLIjY → -pemk30NXpiExEU5tjb1t}/_buildManifest.js +0 -0
- /package/temp/build/pi-desk/release/.next/static/{NwX_o5zDfv-NABRLmLIjY → -pemk30NXpiExEU5tjb1t}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -23,11 +23,11 @@ Pi powers the agent. Pi Desk provides the interface, with a lightweight core and
|
|
|
23
23
|
## Interface demo
|
|
24
24
|
|
|
25
25
|
<p>
|
|
26
|
-
<a href="./docs/
|
|
27
|
-
<img src="./docs/
|
|
26
|
+
<a href="./docs/doc_asset/client-overview.webp">
|
|
27
|
+
<img src="./docs/doc_asset/client-overview.webp" alt="File preview" width="300" />
|
|
28
28
|
</a>
|
|
29
|
-
<a href="./docs/
|
|
30
|
-
<img src="./docs/
|
|
29
|
+
<a href="./docs/doc_asset/multi-window.webp">
|
|
30
|
+
<img src="./docs/doc_asset/multi-window.webp" alt="Side-by-side conversations" width="300" />
|
|
31
31
|
</a>
|
|
32
32
|
</p>
|
|
33
33
|
|
package/README.zh-CN.md
CHANGED
|
@@ -23,11 +23,11 @@ Pi 负责代理运行,Pi Desk 提供围绕它的工作界面。核心保持轻
|
|
|
23
23
|
## 界面演示
|
|
24
24
|
|
|
25
25
|
<p>
|
|
26
|
-
<a href="./docs/
|
|
27
|
-
<img src="./docs/
|
|
26
|
+
<a href="./docs/doc_asset/client-overview.webp">
|
|
27
|
+
<img src="./docs/doc_asset/client-overview.webp" alt="文件预览" width="300" />
|
|
28
28
|
</a>
|
|
29
|
-
<a href="./docs/
|
|
30
|
-
<img src="./docs/
|
|
29
|
+
<a href="./docs/doc_asset/multi-window.webp">
|
|
30
|
+
<img src="./docs/doc_asset/multi-window.webp" alt="多窗口会话" width="300" />
|
|
31
31
|
</a>
|
|
32
32
|
</p>
|
|
33
33
|
|
package/docs/README.md
ADDED
|
Binary file
|
|
Binary file
|
|
@@ -112,7 +112,114 @@ Push 可丢失、不持久化、不 ACK、不重放,也不创建聊天消息
|
|
|
112
112
|
- 当前页面轻提示:`plugin.notify({ level: 'success', title: '保存成功' })` 或 `host.notify()`,只在当前页面短暂展示,离线可错过。
|
|
113
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 重启或插件释放后清空,不持久化。
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
## 消息展示
|
|
116
|
+
|
|
117
|
+
Node Entry 的 Declaration 把已有 Pi 消息转换为 `{ viewKey, summary, detail }`;Browser 的 Message View 按 `viewKey` 展示。`viewKey` 使用小写 kebab-case 的 `namespace/view-name` 形式,标识展示数据协议,不是消息 ID。Declaration 不创建消息,也不管理消息身份。
|
|
118
|
+
|
|
119
|
+
以原生插件产生的 `customType = 'example-status'` 消息为例,在 Node Entry 的 `setup(plugin)` 中注册:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
plugin.declareMessage('status', {
|
|
123
|
+
priority: 100,
|
|
124
|
+
match({ message }) {
|
|
125
|
+
return message.kind === 'custom' && message.customType === 'example-status'
|
|
126
|
+
},
|
|
127
|
+
project({ defaultProjection }) {
|
|
128
|
+
return {
|
|
129
|
+
viewKey: 'example/status',
|
|
130
|
+
summary: defaultProjection.summary,
|
|
131
|
+
detail: null
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
此例复用默认自定义消息的 `summary.text`。在 Browser Entry 同步注册对应视图:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
plugin.registerContribution('message-view', 'status', {
|
|
141
|
+
viewKey: 'example/status',
|
|
142
|
+
priority: 100,
|
|
143
|
+
load: () => import('./status-view.js').then((module) => module.default)
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
同目录的 `status-view.js`:
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
export default {
|
|
151
|
+
mount({ container, target }) {
|
|
152
|
+
const element = document.createElement('p')
|
|
153
|
+
container.append(element)
|
|
154
|
+
|
|
155
|
+
function render() {
|
|
156
|
+
const { text } = target.message.getSnapshot().summary
|
|
157
|
+
if (typeof text !== 'string') {
|
|
158
|
+
target.reportError(new Error('消息摘要缺少 text'))
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
element.textContent = text
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const unsubscribe = target.message.subscribe(render)
|
|
165
|
+
render()
|
|
166
|
+
return () => {
|
|
167
|
+
unsubscribe()
|
|
168
|
+
element.remove()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- `match/project` 必须同步、快速、无副作用;每次返回完整投影,不自行计算或传输增量。自定义数据的结构由对应 `viewKey` 的 Declaration/View 约定,不扩充通用消息类型。
|
|
175
|
+
- 首屏和实时必需内容放 `summary`;大内容或用户展开后才用的数据放 `detail`。需要详情时,仅在消息已有 `entryId` 且 `fixed.hasDetail` 为真后调用 `target.message.loadDetail()`;异步结果须检查挂载的 `signal`,不能写回失效视图。
|
|
176
|
+
- 视图从 `getSnapshot()` 读取完整当前状态,并通过 `subscribe()` 跟随更新;同 `viewKey` 的消息更新不重新挂载,Temporary 转 Durable 保持同一 Handle,`viewKey` 改变时会重挂。释放时解除订阅并清理自有资源。
|
|
177
|
+
- Declaration 和 View 分别按 `priority` 选择,数值越大越优先;同最高优先级冲突会报错。声明、加载或展示失败不自动尝试低优先级实现;异步展示错误通过 `target.reportError(error)` 交给宿主处理。
|
|
178
|
+
- 刷新后仍需展示的数据必须存在于最终 Pi Entry,仅通过 Tool `onUpdate()` 提供的数据不会自动持久化。更新插件时兼容已有消息结构,不靠自动重载旧会话解决兼容问题。
|
|
179
|
+
|
|
180
|
+
## 任务
|
|
181
|
+
|
|
182
|
+
Task Center 用于当前 Session 的真实任务,通过 `bindSessionPlugin()` 返回的 `plugin` 上报;Global 插件的任务由自身运行态管理,不创建 Session Task。
|
|
183
|
+
|
|
184
|
+
在真实操作开始时创建任务,完成后上报结果。以下 `runTask()` 是插件已有的业务函数:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const task = plugin.startTask({
|
|
188
|
+
title: '处理文件',
|
|
189
|
+
activity: '正在执行'
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
await runTask()
|
|
194
|
+
task.complete('处理完成')
|
|
195
|
+
} catch (error) {
|
|
196
|
+
task.fail(error instanceof Error ? error.message : String(error))
|
|
197
|
+
throw error
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
- 进度使用 `task.update({ activity, info })`;`info` 是 `{ label, value }[]`,整体替换,`[]` 清空,不放日志正文。
|
|
202
|
+
- 有停止能力时,给 `startTask()` 的 `interrupt` 传入真实停止函数,签名为 `() => Promise<void>`。回调返回不代表任务已经结束;确认实际停止后调用 `task.stopped()`,会话关闭等中断使用 `task.interrupted()`。
|
|
203
|
+
- 一个任务只进入一次终态,终态后不能 `update()` 或再次结束;`remove()` 只移除上报记录,不停止真实操作。
|
|
204
|
+
- `detailSource` 可选:文本日志用 `{ kind: 'text-file', path: logPath }`;活动子会话用 `{ kind: 'pi-conversation', session: childSession }`;历史会话用 `{ kind: 'pi-conversation', sessionFile }`。文件路径均为服务端绝对路径,活动实例和文件来源二选一。
|
|
205
|
+
- 宿主只观察详情,不接管插件创建的资源。会话关闭时停止自有任务、上报终态并释放子会话等资源,不关闭父会话;需要随分支结束的任务在树导航前收敛。
|
|
206
|
+
- 任务列表是运行态,不是持久历史;宿主保留全部运行中任务和最近 100 条终态,不能依赖更早记录一直存在。
|
|
207
|
+
|
|
208
|
+
## 日志
|
|
209
|
+
|
|
210
|
+
命令、构建等任务的完整输出由插件写入日志文件,并负责容量与清理。界面复用宿主日志能力,不自行读取文件或实现监听、重连。
|
|
211
|
+
|
|
212
|
+
从 `@jetcrab/pi-desk-sdk/react/base` 导入 `PluginLogViewer`,在已有 `PluginHostProvider` 内使用:
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
<PluginLogViewer path={logPath} />
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`logPath` 是服务端可访问的绝对文本文件路径。文件未创建时继续等待;组件处理追加、重写、重连与读取重试,最多显示末尾 128KB、1000 行,不修改原文件。
|
|
219
|
+
|
|
220
|
+
非 React 界面使用 `host.logs.open(logPath)`,通过返回句柄的 `getSnapshot()`、`subscribe()` 和 `retry()` 读取与观察,释放时调用 `dispose()`。Session Task 可使用 `detailSource: { kind: 'text-file', path: logPath }` 展示同一日志。
|
|
221
|
+
|
|
222
|
+
业务数据只传递必要的日志摘要和路径,不把日志正文放入 Method 返回、Plugin Push、状态槽位或 Pi JSONL。
|
|
116
223
|
|
|
117
224
|
## 更新生效
|
|
118
225
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jetcrab/pi-desk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8-dev",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"bin",
|
|
22
|
-
"docs
|
|
22
|
+
"docs",
|
|
23
23
|
"next.config.ts",
|
|
24
24
|
"package.json",
|
|
25
25
|
"src",
|
|
@@ -221,6 +221,8 @@ export function L2ProjectPreviewDialog({
|
|
|
221
221
|
const code = (
|
|
222
222
|
<div className="flex size-full min-h-0 min-w-0 flex-col">
|
|
223
223
|
<L3ProjectFileTabs
|
|
224
|
+
cwd={cwd}
|
|
225
|
+
gitRuntime={git}
|
|
224
226
|
paths={state.tabs}
|
|
225
227
|
activePath={state.activePath}
|
|
226
228
|
diffPath={diffPath}
|
|
@@ -230,6 +232,7 @@ export function L2ProjectPreviewDialog({
|
|
|
230
232
|
/>
|
|
231
233
|
<L3ProjectDocumentView
|
|
232
234
|
runtime={files}
|
|
235
|
+
gitRuntime={git}
|
|
233
236
|
compact={size.width < 1100 || narrow}
|
|
234
237
|
onEscape={onClose}
|
|
235
238
|
/>
|
|
@@ -207,6 +207,8 @@ export function L2FileWorkspaceView({
|
|
|
207
207
|
) : null}
|
|
208
208
|
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
|
209
209
|
<L2FileWorkspaceTabs
|
|
210
|
+
cwd={cwd}
|
|
211
|
+
gitRuntime={gitRuntime}
|
|
210
212
|
paths={workspace.tabs}
|
|
211
213
|
activePath={activePath}
|
|
212
214
|
diffPath={diffPath}
|
|
@@ -217,6 +219,12 @@ export function L2FileWorkspaceView({
|
|
|
217
219
|
<L3ProjectDocumentView
|
|
218
220
|
ref={documentRef}
|
|
219
221
|
runtime={runtime.getFileRuntime(workId)}
|
|
222
|
+
gitRuntime={gitRuntime}
|
|
223
|
+
visible={
|
|
224
|
+
expanded ||
|
|
225
|
+
(workspace.windowOpen &&
|
|
226
|
+
(presentation === 'split' || workspace.activePane === 'files'))
|
|
227
|
+
}
|
|
220
228
|
compact={compact}
|
|
221
229
|
onEscape={escape}
|
|
222
230
|
onSwitchPane={onSwitchPane}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore, type RefObject } from 'react'
|
|
4
|
+
import type { L3ProjectFileRuntime } from './l3-project-file-runtime'
|
|
5
|
+
import type { L3ProjectGitBrowser } from './l3-project-git-runtime'
|
|
6
|
+
import { findL3GitFile } from './l3-project-git-model'
|
|
7
|
+
|
|
8
|
+
export function useL3ProjectDocumentGit(
|
|
9
|
+
runtime: L3ProjectFileRuntime,
|
|
10
|
+
git: L3ProjectGitBrowser,
|
|
11
|
+
host: RefObject<HTMLDivElement | null>,
|
|
12
|
+
savePosition: () => void,
|
|
13
|
+
active: boolean
|
|
14
|
+
): { refresh: () => void; unavailable: boolean } {
|
|
15
|
+
useSyncExternalStore(git.subscribe, git.getRevision, git.getRevision)
|
|
16
|
+
const snapshot = git.getWorkspace(runtime.cwd)?.snapshot
|
|
17
|
+
const path = runtime.state.activePath
|
|
18
|
+
const diffActive = Boolean(runtime.state.activeDiff)
|
|
19
|
+
const document = path ? runtime.state.documents.get(path) : undefined
|
|
20
|
+
const save = useRef(savePosition)
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
save.current = savePosition
|
|
23
|
+
}, [savePosition])
|
|
24
|
+
const refresh = useCallback((): void => {
|
|
25
|
+
if (!path || diffActive) return
|
|
26
|
+
save.current()
|
|
27
|
+
runtime.reloadFile(path)
|
|
28
|
+
void git.refreshSnapshot(runtime.cwd)
|
|
29
|
+
}, [diffActive, git, path, runtime])
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!active || !path || diffActive) return
|
|
33
|
+
const visible = (): boolean =>
|
|
34
|
+
window.document.visibilityState === 'visible' &&
|
|
35
|
+
Boolean(host.current?.getClientRects().length) &&
|
|
36
|
+
!host.current?.closest('[inert], [aria-hidden="true"]')
|
|
37
|
+
let lastRefresh = Date.now()
|
|
38
|
+
if (visible()) {
|
|
39
|
+
// 已打开的标签重新展示时重读;首次打开仍复用正在进行的正文请求。
|
|
40
|
+
if (runtime.state.documents.get(path)?.status === 'text') {
|
|
41
|
+
save.current()
|
|
42
|
+
runtime.reloadFile(path)
|
|
43
|
+
}
|
|
44
|
+
void git.refreshSnapshot(runtime.cwd)
|
|
45
|
+
}
|
|
46
|
+
const onFocus = (): void => {
|
|
47
|
+
if (!visible() || Date.now() - lastRefresh < 1000) return
|
|
48
|
+
lastRefresh = Date.now()
|
|
49
|
+
refresh()
|
|
50
|
+
}
|
|
51
|
+
window.addEventListener('focus', onFocus)
|
|
52
|
+
window.document.addEventListener('visibilitychange', onFocus)
|
|
53
|
+
return () => {
|
|
54
|
+
window.removeEventListener('focus', onFocus)
|
|
55
|
+
window.document.removeEventListener('visibilitychange', onFocus)
|
|
56
|
+
}
|
|
57
|
+
}, [active, diffActive, git, host, path, refresh, runtime])
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!active || !path || diffActive || document?.status !== 'text') return
|
|
61
|
+
const controller = new AbortController()
|
|
62
|
+
const file =
|
|
63
|
+
snapshot?.status === 'ready' ? findL3GitFile(runtime.cwd, path, snapshot.repositories) : null
|
|
64
|
+
void runtime.compareFile(
|
|
65
|
+
path,
|
|
66
|
+
file?.repository.state === 'ready' && file.change
|
|
67
|
+
? { repositoryRoot: file.repository.root, path: file.change.path }
|
|
68
|
+
: null,
|
|
69
|
+
controller.signal
|
|
70
|
+
)
|
|
71
|
+
return () => controller.abort()
|
|
72
|
+
}, [active, diffActive, document, path, runtime, snapshot])
|
|
73
|
+
|
|
74
|
+
const file =
|
|
75
|
+
path && snapshot?.status === 'ready'
|
|
76
|
+
? findL3GitFile(runtime.cwd, path, snapshot.repositories)
|
|
77
|
+
: null
|
|
78
|
+
return {
|
|
79
|
+
refresh,
|
|
80
|
+
unavailable: snapshot?.status === 'error' || file?.repository.state === 'unavailable'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -6,13 +6,21 @@ import {
|
|
|
6
6
|
CopyIcon,
|
|
7
7
|
FileCode2Icon,
|
|
8
8
|
FoldVerticalIcon,
|
|
9
|
+
GitCompareArrowsIcon,
|
|
9
10
|
ListOrderedIcon,
|
|
10
11
|
LoaderCircleIcon,
|
|
11
12
|
RefreshCwIcon,
|
|
12
13
|
SearchIcon,
|
|
13
14
|
WrapTextIcon
|
|
14
15
|
} from 'lucide-react'
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
useEffect,
|
|
18
|
+
useImperativeHandle,
|
|
19
|
+
useRef,
|
|
20
|
+
useState,
|
|
21
|
+
useSyncExternalStore,
|
|
22
|
+
type Ref
|
|
23
|
+
} from 'react'
|
|
16
24
|
import { useTranslation } from 'react-i18next'
|
|
17
25
|
import { copyL4BrowserText } from '@client/l4_foundation/lib/l4-browser-clipboard'
|
|
18
26
|
import {
|
|
@@ -25,6 +33,8 @@ import { L4ImageViewer } from '@client/l4_foundation/ui/l4-image-viewer'
|
|
|
25
33
|
import { useL4AppToast } from '@client/l4_foundation/ui/l4-app-toast'
|
|
26
34
|
import { Button } from '@client/l4_foundation/ui/shadcn/button'
|
|
27
35
|
import type { L3ProjectFileRuntime } from './l3-project-file-runtime'
|
|
36
|
+
import type { L3ProjectGitBrowser } from './l3-project-git-runtime'
|
|
37
|
+
import { useL3ProjectDocumentGit } from './l3-project-document-git'
|
|
28
38
|
import { L3CodeSurfaceToolbarButton } from './l3-code-surface-toolbar-button'
|
|
29
39
|
import { buildL3FilePreviewHtml } from './l3-file-preview-html'
|
|
30
40
|
|
|
@@ -38,13 +48,17 @@ export interface L3ProjectDocumentHandle {
|
|
|
38
48
|
export function L3ProjectDocumentView({
|
|
39
49
|
ref: handleRef,
|
|
40
50
|
runtime,
|
|
51
|
+
gitRuntime,
|
|
41
52
|
compact,
|
|
53
|
+
visible = true,
|
|
42
54
|
onEscape,
|
|
43
55
|
onSwitchPane
|
|
44
56
|
}: {
|
|
45
57
|
ref?: Ref<L3ProjectDocumentHandle>
|
|
46
58
|
runtime: L3ProjectFileRuntime
|
|
59
|
+
gitRuntime: L3ProjectGitBrowser
|
|
47
60
|
compact: boolean
|
|
61
|
+
visible?: boolean
|
|
48
62
|
onEscape: () => void
|
|
49
63
|
onSwitchPane?: () => void
|
|
50
64
|
}): React.JSX.Element {
|
|
@@ -55,6 +69,7 @@ export function L3ProjectDocumentView({
|
|
|
55
69
|
const document = !activeDiff && activePath ? runtime.state.documents.get(activePath) : null
|
|
56
70
|
const isHtml = Boolean(activePath && /\.html?$/i.test(activePath))
|
|
57
71
|
const showSource = document?.status === 'text' && (!isHtml || document.viewMode === 'source')
|
|
72
|
+
const hostRef = useRef<HTMLDivElement>(null)
|
|
58
73
|
const previewRef = useRef<L4CodePreviewHandle>(null)
|
|
59
74
|
const diffRef = useRef<L4CodeDiffHandle>(null)
|
|
60
75
|
const iframeRef = useRef<HTMLIFrameElement>(null)
|
|
@@ -83,6 +98,42 @@ export function L3ProjectDocumentView({
|
|
|
83
98
|
if (activePath && previewRef.current)
|
|
84
99
|
runtime.saveEditorState(activePath, previewRef.current.getViewState())
|
|
85
100
|
}
|
|
101
|
+
const { refresh, unavailable } = useL3ProjectDocumentGit(
|
|
102
|
+
runtime,
|
|
103
|
+
gitRuntime,
|
|
104
|
+
hostRef,
|
|
105
|
+
savePosition,
|
|
106
|
+
visible
|
|
107
|
+
)
|
|
108
|
+
const comparison = document?.status === 'text' ? document.comparison : undefined
|
|
109
|
+
const [computed, setComputed] = useState<{
|
|
110
|
+
original: string
|
|
111
|
+
modified: string
|
|
112
|
+
count: number | null
|
|
113
|
+
} | null>(null)
|
|
114
|
+
const count =
|
|
115
|
+
comparison?.status === 'ready' &&
|
|
116
|
+
document?.status === 'text' &&
|
|
117
|
+
computed?.original === comparison.original.content &&
|
|
118
|
+
computed.modified === document.content
|
|
119
|
+
? computed.count
|
|
120
|
+
: undefined
|
|
121
|
+
const openDiff = (line?: number): void => {
|
|
122
|
+
if (!activePath) return
|
|
123
|
+
savePosition()
|
|
124
|
+
runtime.openFileDiff(activePath, line)
|
|
125
|
+
}
|
|
126
|
+
const gitStatus =
|
|
127
|
+
unavailable || comparison?.status === 'error'
|
|
128
|
+
? t('gitMarkersFailed')
|
|
129
|
+
: comparison?.status === 'unavailable' || (comparison?.status === 'ready' && count === null)
|
|
130
|
+
? t('gitMarkersUnavailable')
|
|
131
|
+
: comparison?.status === 'loading' ||
|
|
132
|
+
(comparison?.status === 'ready' && count === undefined)
|
|
133
|
+
? t('gitMarkersLoading')
|
|
134
|
+
: comparison?.status === 'ready'
|
|
135
|
+
? t('gitChangesCount', { count })
|
|
136
|
+
: null
|
|
86
137
|
const copyText = (): void => {
|
|
87
138
|
if (document?.status !== 'text') return
|
|
88
139
|
const selection = previewRef.current?.getSelectionText() ?? ''
|
|
@@ -93,6 +144,7 @@ export function L3ProjectDocumentView({
|
|
|
93
144
|
}
|
|
94
145
|
return (
|
|
95
146
|
<div
|
|
147
|
+
ref={hostRef}
|
|
96
148
|
className="flex size-full min-h-0 min-w-0 flex-col outline-none"
|
|
97
149
|
tabIndex={-1}
|
|
98
150
|
onFocus={(event) => {
|
|
@@ -181,6 +233,41 @@ export function L3ProjectDocumentView({
|
|
|
181
233
|
))}
|
|
182
234
|
</div>
|
|
183
235
|
) : null}
|
|
236
|
+
{showSource && gitStatus ? (
|
|
237
|
+
<span
|
|
238
|
+
data-testid="file-git-status"
|
|
239
|
+
role="status"
|
|
240
|
+
className="mr-auto min-w-0 truncate text-xs text-muted-foreground"
|
|
241
|
+
title={comparison?.status === 'error' ? comparison.error : gitStatus}
|
|
242
|
+
>
|
|
243
|
+
{compact && comparison?.status === 'ready' && typeof count === 'number'
|
|
244
|
+
? t('gitChangesCompact', { count })
|
|
245
|
+
: gitStatus}
|
|
246
|
+
</span>
|
|
247
|
+
) : null}
|
|
248
|
+
{showSource && comparison?.status === 'ready' ? (
|
|
249
|
+
<>
|
|
250
|
+
{typeof count === 'number' && count > 0 ? (
|
|
251
|
+
<>
|
|
252
|
+
<L3CodeSurfaceToolbarButton
|
|
253
|
+
label={t('previousChange')}
|
|
254
|
+
onClick={() => previewRef.current?.previousChange()}
|
|
255
|
+
>
|
|
256
|
+
<ArrowUpIcon className="size-4" />
|
|
257
|
+
</L3CodeSurfaceToolbarButton>
|
|
258
|
+
<L3CodeSurfaceToolbarButton
|
|
259
|
+
label={t('nextChange')}
|
|
260
|
+
onClick={() => previewRef.current?.nextChange()}
|
|
261
|
+
>
|
|
262
|
+
<ArrowDownIcon className="size-4" />
|
|
263
|
+
</L3CodeSurfaceToolbarButton>
|
|
264
|
+
</>
|
|
265
|
+
) : null}
|
|
266
|
+
<L3CodeSurfaceToolbarButton label={t('viewDiff')} onClick={() => openDiff()}>
|
|
267
|
+
<GitCompareArrowsIcon className="size-4" />
|
|
268
|
+
</L3CodeSurfaceToolbarButton>
|
|
269
|
+
</>
|
|
270
|
+
) : null}
|
|
184
271
|
{showSource && activePath ? (
|
|
185
272
|
<>
|
|
186
273
|
<L3CodeSurfaceToolbarButton
|
|
@@ -210,10 +297,7 @@ export function L3ProjectDocumentView({
|
|
|
210
297
|
</>
|
|
211
298
|
) : null}
|
|
212
299
|
{activePath ? (
|
|
213
|
-
<L3CodeSurfaceToolbarButton
|
|
214
|
-
label={t('reloadCurrentFile')}
|
|
215
|
-
onClick={() => runtime.reloadFile(activePath)}
|
|
216
|
-
>
|
|
300
|
+
<L3CodeSurfaceToolbarButton label={t('reloadCurrentFile')} onClick={refresh}>
|
|
217
301
|
<RefreshCwIcon className="size-4" />
|
|
218
302
|
</L3CodeSurfaceToolbarButton>
|
|
219
303
|
) : null}
|
|
@@ -231,6 +315,7 @@ export function L3ProjectDocumentView({
|
|
|
231
315
|
modified={activeDiff.modified}
|
|
232
316
|
viewMode={compact ? 'inline' : diffViewMode}
|
|
233
317
|
savedState={activeDiff.editorState}
|
|
318
|
+
initialLine={activeDiff.initialLine}
|
|
234
319
|
onSaveState={(position) => runtime.saveDiffState(activeDiff, position)}
|
|
235
320
|
onEscape={escape}
|
|
236
321
|
/>
|
|
@@ -302,6 +387,18 @@ export function L3ProjectDocumentView({
|
|
|
302
387
|
path={activePath}
|
|
303
388
|
content={document.content}
|
|
304
389
|
size={document.size}
|
|
390
|
+
originalContent={
|
|
391
|
+
comparison?.status === 'ready' ? comparison.original.content : undefined
|
|
392
|
+
}
|
|
393
|
+
onChanges={(count) => {
|
|
394
|
+
if (comparison?.status === 'ready')
|
|
395
|
+
setComputed({
|
|
396
|
+
original: comparison.original.content,
|
|
397
|
+
modified: document.content,
|
|
398
|
+
count
|
|
399
|
+
})
|
|
400
|
+
}}
|
|
401
|
+
onOpenDiff={openDiff}
|
|
305
402
|
savedState={document.editorState}
|
|
306
403
|
onSaveState={(path, state) => runtime.saveEditorState(path, state)}
|
|
307
404
|
onEscape={escape}
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
import type { L3ProjectGitBrowser } from './l3-project-git-runtime'
|
|
40
40
|
import {
|
|
41
41
|
getL3GitChangeBadge,
|
|
42
|
+
getL3GitChangeClass,
|
|
42
43
|
getL3GitHeadLabel,
|
|
43
44
|
getL3GitRepositoryName,
|
|
44
45
|
joinL3GitProjectPath,
|
|
@@ -118,12 +119,7 @@ function fileIcon(path: string): React.JSX.Element {
|
|
|
118
119
|
|
|
119
120
|
function changedFileNameClass(change: L3GitProjectChange | null): string | undefined {
|
|
120
121
|
if (!change) return undefined
|
|
121
|
-
|
|
122
|
-
if (badge === 'D') return 'text-muted-foreground'
|
|
123
|
-
if (badge === '?') return 'text-red-600 dark:text-red-400'
|
|
124
|
-
if (badge === 'A') return 'text-emerald-600 dark:text-emerald-400'
|
|
125
|
-
if (badge === '!') return 'text-violet-600 dark:text-violet-400'
|
|
126
|
-
return 'text-blue-600 dark:text-blue-400'
|
|
122
|
+
return getL3GitChangeClass(change.change)
|
|
127
123
|
}
|
|
128
124
|
|
|
129
125
|
function DirectoryFeedback({
|
|
@@ -22,6 +22,16 @@ export type L3DirectoryState =
|
|
|
22
22
|
| { status: 'idle' | 'loading' }
|
|
23
23
|
| { status: 'ready'; entries: L4ProjectFileEntry[]; truncated: boolean }
|
|
24
24
|
| { status: 'error'; error: string }
|
|
25
|
+
export type L3FileComparison =
|
|
26
|
+
| { status: 'loading' }
|
|
27
|
+
| {
|
|
28
|
+
status: 'ready'
|
|
29
|
+
repositoryRoot: string
|
|
30
|
+
path: string
|
|
31
|
+
original: { path: string; content: string }
|
|
32
|
+
}
|
|
33
|
+
| { status: 'unavailable' }
|
|
34
|
+
| { status: 'error'; error: string }
|
|
25
35
|
export type L3FileDocument =
|
|
26
36
|
| { status: 'unloaded' }
|
|
27
37
|
| { status: 'loading' }
|
|
@@ -31,6 +41,7 @@ export type L3FileDocument =
|
|
|
31
41
|
size: number
|
|
32
42
|
editorState: L4CodePreviewState | null
|
|
33
43
|
viewMode: 'source' | 'preview'
|
|
44
|
+
comparison?: L3FileComparison
|
|
34
45
|
}
|
|
35
46
|
| { status: 'image'; mimeType: L4ProjectFileImageMimeType; size: number; objectUrl: string }
|
|
36
47
|
| { status: 'error'; error: string }
|
|
@@ -47,6 +58,7 @@ interface DiffOptions {
|
|
|
47
58
|
comparison?: L4GitHistoricalDiff
|
|
48
59
|
editorState?: L4CodeDiffState | null
|
|
49
60
|
label?: string
|
|
61
|
+
initialLine?: number
|
|
50
62
|
}
|
|
51
63
|
export type L3FileDiff = DiffOptions &
|
|
52
64
|
(
|
|
@@ -492,6 +504,71 @@ export class L3ProjectFileRuntime {
|
|
|
492
504
|
}
|
|
493
505
|
this.publish()
|
|
494
506
|
}
|
|
507
|
+
async compareFile(
|
|
508
|
+
path: string,
|
|
509
|
+
target: { repositoryRoot: string; path: string } | null,
|
|
510
|
+
signal: AbortSignal
|
|
511
|
+
): Promise<void> {
|
|
512
|
+
const document = this.state.documents.get(path)
|
|
513
|
+
if (this.disposed || signal.aborted || document?.status !== 'text') return
|
|
514
|
+
if (!target) {
|
|
515
|
+
if (document.comparison) {
|
|
516
|
+
delete document.comparison
|
|
517
|
+
this.publish()
|
|
518
|
+
}
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
const loading: L3FileComparison = { status: 'loading' }
|
|
522
|
+
document.comparison = loading
|
|
523
|
+
this.publish()
|
|
524
|
+
const current = (): boolean =>
|
|
525
|
+
!this.disposed &&
|
|
526
|
+
!signal.aborted &&
|
|
527
|
+
this.state.documents.get(path) === document &&
|
|
528
|
+
document.comparison === loading
|
|
529
|
+
try {
|
|
530
|
+
const result = await this.reader.diff(target.repositoryRoot, target.path, signal)
|
|
531
|
+
if (!current()) return
|
|
532
|
+
if (result.kind === 'text') {
|
|
533
|
+
// 比较结果的工作区正文与基线一起替换,不能把新行号画在缓存正文上。
|
|
534
|
+
document.content = result.modified.content
|
|
535
|
+
document.size = new TextEncoder().encode(result.modified.content).length
|
|
536
|
+
document.comparison = {
|
|
537
|
+
status: 'ready',
|
|
538
|
+
repositoryRoot: target.repositoryRoot,
|
|
539
|
+
path: target.path,
|
|
540
|
+
original: result.original
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
document.comparison = { status: 'unavailable' }
|
|
544
|
+
}
|
|
545
|
+
} catch (cause) {
|
|
546
|
+
if (!current()) return
|
|
547
|
+
console.warn('[Pi Desk][ProjectFile] 读取行变更失败', {
|
|
548
|
+
cwd: this.cwd,
|
|
549
|
+
path,
|
|
550
|
+
error: message(cause)
|
|
551
|
+
})
|
|
552
|
+
document.comparison = { status: 'error', error: message(cause) }
|
|
553
|
+
}
|
|
554
|
+
this.publish()
|
|
555
|
+
}
|
|
556
|
+
openFileDiff(path: string, initialLine?: number): void {
|
|
557
|
+
const document = this.state.documents.get(path)
|
|
558
|
+
if (this.disposed || document?.status !== 'text' || document.comparison?.status !== 'ready')
|
|
559
|
+
return
|
|
560
|
+
const comparison = document.comparison
|
|
561
|
+
this.clearDiff()
|
|
562
|
+
this.state.activeDiff = {
|
|
563
|
+
status: 'text',
|
|
564
|
+
repositoryRoot: comparison.repositoryRoot,
|
|
565
|
+
path: comparison.path,
|
|
566
|
+
original: comparison.original,
|
|
567
|
+
modified: { path: comparison.path, content: document.content },
|
|
568
|
+
initialLine
|
|
569
|
+
}
|
|
570
|
+
this.publish()
|
|
571
|
+
}
|
|
495
572
|
openDiff(
|
|
496
573
|
repositoryRoot: string,
|
|
497
574
|
path: string,
|
|
@@ -616,7 +693,12 @@ export class L3ProjectFileRuntime {
|
|
|
616
693
|
private invalidateDocument(path: string): void {
|
|
617
694
|
this.documentRequests.get(path)?.abort()
|
|
618
695
|
this.documentRequests.delete(path)
|
|
619
|
-
this.
|
|
696
|
+
const document = this.state.documents.get(path)
|
|
697
|
+
if (document?.status === 'text') {
|
|
698
|
+
if (document.editorState) this.restoredEditorStates.set(path, document.editorState)
|
|
699
|
+
this.restoredFileViewModes.set(path, document.viewMode)
|
|
700
|
+
}
|
|
701
|
+
this.releaseDocument(document)
|
|
620
702
|
this.state.documents.set(path, { status: 'unloaded' })
|
|
621
703
|
}
|
|
622
704
|
private releaseDocument(document: L3FileDocument | undefined): void {
|