@jetcrab/pi-desk 1.1.20-dev → 1.1.22-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.
Files changed (202) hide show
  1. package/docs/plugins.md +81 -1
  2. package/package.json +3 -3
  3. package/src/app/page.tsx +6 -1
  4. package/src/app/project-preview/page.tsx +10 -0
  5. package/src/client/l1_entry/pages/l1-home-page.tsx +30 -0
  6. package/src/client/l1_entry/pages/l1-project-preview-page.tsx +33 -0
  7. package/src/client/l1_entry/providers/l1-app-providers.tsx +18 -15
  8. package/src/client/l1_entry/providers/l1-app-runtime-provider.tsx +2 -2
  9. package/src/client/l2_biz/auth/l2-login-biz.ts +3 -0
  10. package/src/client/l2_biz/auth/l2-login.tsx +2 -3
  11. package/src/client/l2_biz/plugin-host/l2-plugin-capability-details.tsx +6 -1
  12. package/src/client/l2_biz/plugin-host/l2-plugin-contribution-host.tsx +71 -10
  13. package/src/client/l2_biz/plugin-host/l2-plugin-management-controls.tsx +9 -0
  14. package/src/client/l2_biz/plugin-host/l2-plugin-session-header-layout.ts +18 -0
  15. package/src/client/l2_biz/plugin-host/l2-plugin-session-header.tsx +247 -0
  16. package/src/client/l2_biz/plugin-host/l2-plugin-widgets.tsx +50 -0
  17. package/src/client/l2_biz/plugin-host/views/l2-plugin-session-header-action.tsx +112 -0
  18. package/src/client/l2_biz/project-preview/hooks/l2-use-project-picker.ts +76 -0
  19. package/src/client/l2_biz/project-preview/hooks/l2-use-project-preview.ts +214 -0
  20. package/src/client/l2_biz/project-preview/l2-project-preview-biz.ts +49 -4
  21. package/src/client/l2_biz/project-preview/l2-project-preview-dialog.tsx +42 -450
  22. package/src/client/l2_biz/project-preview/l2-project-preview-page.tsx +61 -0
  23. package/src/client/l2_biz/project-preview/l2-project-preview.tsx +342 -0
  24. package/src/client/l2_biz/project-preview/runtime/l2-project-history-runtime.ts +9 -3
  25. package/src/client/l2_biz/project-preview/views/l2-project-picker.tsx +168 -0
  26. package/src/client/l2_biz/settings/hooks/l2-use-model-settings.ts +34 -31
  27. package/src/client/l2_biz/settings/views/l2-model-replacement-preview.tsx +7 -1
  28. package/src/client/l2_biz/workbench/file-workspace/l2-project-preview-biz.ts +4 -3
  29. package/src/client/l2_biz/workbench/hooks/l2-workbench-chat-viewport.ts +28 -21
  30. package/src/client/l2_biz/workbench/l2-work-session-columns.module.css +32 -0
  31. package/src/client/l2_biz/workbench/l2-work-session-columns.tsx +55 -16
  32. package/src/client/l2_biz/workbench/l2-work-session-sidebar.tsx +19 -8
  33. package/src/client/l2_biz/workbench/l2-workbench-chat-composer.tsx +12 -1
  34. package/src/client/l2_biz/workbench/l2-workbench-initialization-notice.tsx +8 -3
  35. package/src/client/l2_biz/workbench/l2-workbench-plugin-slots.ts +13 -0
  36. package/src/client/l2_biz/workbench/l2-workbench.tsx +15 -2
  37. package/src/client/l3_modules/conversation/l3-conversation-viewport.tsx +12 -6
  38. package/src/client/l3_modules/project-files/l3-markdown-preview.module.css +1 -1
  39. package/src/client/l3_modules/project-files/l3-project-document-git.ts +4 -3
  40. package/src/client/l3_modules/project-files/l3-project-document-view.tsx +4 -1
  41. package/src/client/l3_modules/project-files/l3-project-reader.ts +23 -35
  42. package/src/client/l4_foundation/lib/l4-client-id.tsx +11 -0
  43. package/src/client/l4_foundation/plugin-host/l4-plugin-host-runtime.ts +316 -35
  44. package/src/common/l2_biz/auth/l2-auth-return-to.ts +41 -0
  45. package/src/common/l2_biz/model-settings/l2-model-settings-contract.ts +5 -1
  46. package/src/common/l2_biz/plugin/l2-plugin-management-messages.ts +4 -0
  47. package/src/common/l2_biz/project-preview/l2-project-history-messages.ts +28 -0
  48. package/src/common/l2_biz/project-preview/l2-project-preview-navigation.ts +5 -0
  49. package/src/common/l2_biz/settings/l2-settings-messages.ts +4 -3
  50. package/src/common/l2_biz/workbench/l2-workbench-locale-messages.ts +2 -0
  51. package/src/server/l1_entry/node/l1-server.ts +8 -4
  52. package/src/server/l4_foundation/model-settings/l4-model-settings-store.ts +29 -20
  53. package/src/server/l4_foundation/model-settings/l4-model-settings-types.ts +1 -1
  54. package/src/server/l4_foundation/pi/l4-pi-chat-worker.ts +22 -5
  55. package/src/server/l4_foundation/pi/l4-pi-plugin-owner-runtime.ts +133 -13
  56. package/src/server/l4_foundation/pi/l4-pi-session-initialization.ts +9 -0
  57. package/temp/build/pi-desk/release/.next/BUILD_ID +1 -1
  58. package/temp/build/pi-desk/release/.next/app-path-routes-manifest.json +3 -2
  59. package/temp/build/pi-desk/release/.next/build-manifest.json +3 -3
  60. package/temp/build/pi-desk/release/.next/react-loadable-manifest.json +1 -2
  61. package/temp/build/pi-desk/release/.next/required-server-files.js +2 -2
  62. package/temp/build/pi-desk/release/.next/required-server-files.json +2 -2
  63. package/temp/build/pi-desk/release/.next/routes-manifest.json +6 -0
  64. package/temp/build/pi-desk/release/.next/server/app/_global-error/page.js +1 -1
  65. package/temp/build/pi-desk/release/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  66. package/temp/build/pi-desk/release/.next/server/app/_global-error.html +1 -1
  67. package/temp/build/pi-desk/release/.next/server/app/_global-error.rsc +3 -3
  68. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_full.segment.rsc +3 -3
  69. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +3 -3
  70. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  71. package/temp/build/pi-desk/release/.next/server/app/_not-found/page.js +1 -1
  72. package/temp/build/pi-desk/release/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  73. package/temp/build/pi-desk/release/.next/server/app/api/auth/login/route.js +1 -1
  74. package/temp/build/pi-desk/release/.next/server/app/api/auth/logout/route.js +1 -1
  75. package/temp/build/pi-desk/release/.next/server/app/api/auth-settings/get/route.js +1 -1
  76. package/temp/build/pi-desk/release/.next/server/app/api/auth-settings/replace/route.js +1 -1
  77. package/temp/build/pi-desk/release/.next/server/app/api/capability-modes/get/route.js +1 -1
  78. package/temp/build/pi-desk/release/.next/server/app/api/capability-modes/replace/route.js +1 -1
  79. package/temp/build/pi-desk/release/.next/server/app/api/chat/context/get/route.js +1 -1
  80. package/temp/build/pi-desk/release/.next/server/app/api/chat/images/get/route.js +1 -1
  81. package/temp/build/pi-desk/release/.next/server/app/api/chat/messages/get/route.js +1 -1
  82. package/temp/build/pi-desk/release/.next/server/app/api/health/route.js +1 -1
  83. package/temp/build/pi-desk/release/.next/server/app/api/mcp-check/get/route.js +1 -1
  84. package/temp/build/pi-desk/release/.next/server/app/api/mcp-settings/del/route.js +1 -1
  85. package/temp/build/pi-desk/release/.next/server/app/api/mcp-settings/get/route.js +1 -1
  86. package/temp/build/pi-desk/release/.next/server/app/api/mcp-settings/replace/route.js +1 -1
  87. package/temp/build/pi-desk/release/.next/server/app/api/model-catalog/list/route.js +1 -1
  88. package/temp/build/pi-desk/release/.next/server/app/api/model-settings/get/route.js +1 -1
  89. package/temp/build/pi-desk/release/.next/server/app/api/model-settings/preview/route.js +1 -1
  90. package/temp/build/pi-desk/release/.next/server/app/api/model-settings/replace/route.js +1 -1
  91. package/temp/build/pi-desk/release/.next/server/app/api/pi/directories/list/route.js +1 -1
  92. package/temp/build/pi-desk/release/.next/server/app/api/pi/directory-entries/list/route.js +1 -1
  93. package/temp/build/pi-desk/release/.next/server/app/api/pi/directory-ignores/list/route.js +1 -1
  94. package/temp/build/pi-desk/release/.next/server/app/api/pi/directory-ignores/replace/route.js +1 -1
  95. package/temp/build/pi-desk/release/.next/server/app/api/pi/models/list/route.js +1 -1
  96. package/temp/build/pi-desk/release/.next/server/app/api/pi/session-user-messages/list/route.js +1 -1
  97. package/temp/build/pi-desk/release/.next/server/app/api/pi/sessions/list/route.js +1 -1
  98. package/temp/build/pi-desk/release/.next/server/app/api/plugin-catalog/get/route.js +1 -1
  99. package/temp/build/pi-desk/release/.next/server/app/api/plugin-catalog/search/route.js +1 -1
  100. package/temp/build/pi-desk/release/.next/server/app/api/plugin-catalog/settings-get/route.js +1 -1
  101. package/temp/build/pi-desk/release/.next/server/app/api/plugin-catalog/settings-replace/route.js +1 -1
  102. package/temp/build/pi-desk/release/.next/server/app/api/plugins/add/route.js +1 -1
  103. package/temp/build/pi-desk/release/.next/server/app/api/plugins/apply/route.js +1 -1
  104. package/temp/build/pi-desk/release/.next/server/app/api/plugins/batch/route.js +1 -1
  105. package/temp/build/pi-desk/release/.next/server/app/api/plugins/browser-resources/[resourceGroup]/[...resourcePath]/route.js +1 -1
  106. package/temp/build/pi-desk/release/.next/server/app/api/plugins/del/route.js +1 -1
  107. package/temp/build/pi-desk/release/.next/server/app/api/plugins/enabled-set/route.js +1 -1
  108. package/temp/build/pi-desk/release/.next/server/app/api/plugins/get/route.js +1 -1
  109. package/temp/build/pi-desk/release/.next/server/app/api/plugins/host-runtime/[runtimeVersion]/[...resourcePath]/route.js +1 -1
  110. package/temp/build/pi-desk/release/.next/server/app/api/plugins/list/route.js +1 -1
  111. package/temp/build/pi-desk/release/.next/server/app/api/plugins/reload/route.js +1 -1
  112. package/temp/build/pi-desk/release/.next/server/app/api/plugins/update/route.js +1 -1
  113. package/temp/build/pi-desk/release/.next/server/app/api/project-files/get/route.js +1 -1
  114. package/temp/build/pi-desk/release/.next/server/app/api/project-files/list/route.js +1 -1
  115. package/temp/build/pi-desk/release/.next/server/app/api/project-files/reveal/route.js +1 -1
  116. package/temp/build/pi-desk/release/.next/server/app/api/project-files/search/route.js +2 -2
  117. package/temp/build/pi-desk/release/.next/server/app/api/project-git/get/route.js +1 -1
  118. package/temp/build/pi-desk/release/.next/server/app/api/project-git-branches/list/route.js +1 -1
  119. package/temp/build/pi-desk/release/.next/server/app/api/project-git-changes/list/route.js +1 -1
  120. package/temp/build/pi-desk/release/.next/server/app/api/project-git-diff/get/route.js +2 -2
  121. package/temp/build/pi-desk/release/.next/server/app/api/project-git-heads/get/route.js +1 -1
  122. package/temp/build/pi-desk/release/.next/server/app/api/project-git-log/list/route.js +2 -2
  123. package/temp/build/pi-desk/release/.next/server/app/api/project-model-default/get/route.js +1 -1
  124. package/temp/build/pi-desk/release/.next/server/app/api/project-model-default/replace/route.js +1 -1
  125. package/temp/build/pi-desk/release/.next/server/app/api/skill-files/get/route.js +1 -1
  126. package/temp/build/pi-desk/release/.next/server/app/api/skill-files/list/route.js +1 -1
  127. package/temp/build/pi-desk/release/.next/server/app/api/skill-files/replace/route.js +1 -1
  128. package/temp/build/pi-desk/release/.next/server/app/api/skills/list/route.js +1 -1
  129. package/temp/build/pi-desk/release/.next/server/app/api/task-center/images/get/route.js +1 -1
  130. package/temp/build/pi-desk/release/.next/server/app/api/task-center/messages/get/route.js +1 -1
  131. package/temp/build/pi-desk/release/.next/server/app/api/work-session-git-branch/add/route.js +1 -1
  132. package/temp/build/pi-desk/release/.next/server/app/api/work-session-git-branch/replace/route.js +1 -1
  133. package/temp/build/pi-desk/release/.next/server/app/api/work-session-tree/get/route.js +1 -1
  134. package/temp/build/pi-desk/release/.next/server/app/api/work-session-tree-entry/get/route.js +1 -1
  135. package/temp/build/pi-desk/release/.next/server/app/api/work-sessions/add/route.js +1 -1
  136. package/temp/build/pi-desk/release/.next/server/app/api/work-sessions/branch/route.js +1 -1
  137. package/temp/build/pi-desk/release/.next/server/app/api/work-sessions/del/route.js +1 -1
  138. package/temp/build/pi-desk/release/.next/server/app/api/work-sessions/rename/route.js +1 -1
  139. package/temp/build/pi-desk/release/.next/server/app/api/work-sessions/replace/route.js +1 -1
  140. package/temp/build/pi-desk/release/.next/server/app/api/work-sessions/sort/route.js +1 -1
  141. package/temp/build/pi-desk/release/.next/server/app/favicon.ico/route.js +1 -1
  142. package/temp/build/pi-desk/release/.next/server/app/login/page.js +1 -1
  143. package/temp/build/pi-desk/release/.next/server/app/login/page_client-reference-manifest.js +1 -1
  144. package/temp/build/pi-desk/release/.next/server/app/page.js +140 -140
  145. package/temp/build/pi-desk/release/.next/server/app/page_client-reference-manifest.js +1 -1
  146. package/temp/build/pi-desk/release/.next/server/app/project-preview/page.js +1 -0
  147. package/temp/build/pi-desk/release/.next/server/app/project-preview/page_client-reference-manifest.js +1 -0
  148. package/temp/build/pi-desk/release/.next/server/app-paths-manifest.json +3 -2
  149. package/temp/build/pi-desk/release/.next/server/chunks/10912.js +2 -2
  150. package/temp/build/pi-desk/release/.next/server/chunks/20460.js +15 -0
  151. package/temp/build/pi-desk/release/.next/server/chunks/25406.js +8 -8
  152. package/temp/build/pi-desk/release/.next/server/chunks/35985.js +3 -17
  153. package/temp/build/pi-desk/release/.next/server/chunks/36361.js +1 -1
  154. package/temp/build/pi-desk/release/.next/server/chunks/48180.js +1 -1
  155. package/temp/build/pi-desk/release/.next/server/chunks/56830.js +235 -0
  156. package/temp/build/pi-desk/release/.next/server/chunks/78262.js +1 -1
  157. package/temp/build/pi-desk/release/.next/server/chunks/79686.js +62 -0
  158. package/temp/build/pi-desk/release/.next/server/chunks/80780.js +1 -0
  159. package/temp/build/pi-desk/release/.next/server/chunks/81363.js +1 -1
  160. package/temp/build/pi-desk/release/.next/server/chunks/82499.js +4 -4
  161. package/temp/build/pi-desk/release/.next/server/chunks/92386.js +8 -8
  162. package/temp/build/pi-desk/release/.next/server/middleware-build-manifest.js +1 -1
  163. package/temp/build/pi-desk/release/.next/server/middleware-react-loadable-manifest.js +1 -1
  164. package/temp/build/pi-desk/release/.next/server/pages/500.html +1 -1
  165. package/temp/build/pi-desk/release/.next/static/MV6_lBt7Mzmi1rBNI-3dx/_buildManifest.js +1 -0
  166. package/temp/build/pi-desk/release/.next/static/chunks/11937-1d0a9822f2c7995b.js +1 -0
  167. package/temp/build/pi-desk/release/.next/static/chunks/14825-5579aed34a631ad7.js +62 -0
  168. package/temp/build/pi-desk/release/.next/static/chunks/15302-9c807afc6c60de0c.js +1 -0
  169. package/temp/build/pi-desk/release/.next/static/chunks/19991.8aa2e4b6df15fad6.js +1 -0
  170. package/temp/build/pi-desk/release/.next/static/chunks/22092-6d1bfbefa825ec03.js +1 -0
  171. package/temp/build/pi-desk/release/.next/static/chunks/37313-d5970e2d0c270d22.js +238 -0
  172. package/temp/build/pi-desk/release/.next/static/chunks/57235-03ba1044f22b6c84.js +1 -0
  173. package/temp/build/pi-desk/release/.next/static/chunks/79267-7d21d7cdbd7cfca1.js +1 -0
  174. package/temp/build/pi-desk/release/.next/static/chunks/82371-f7ba4a07a5dbcf34.js +1 -0
  175. package/temp/build/pi-desk/release/.next/static/chunks/9038-03144ed96d613f75.js +15 -0
  176. package/temp/build/pi-desk/release/.next/static/chunks/app/{error-0296a3b20cd1bf00.js → error-216b14b5da089ed3.js} +1 -1
  177. package/temp/build/pi-desk/release/.next/static/chunks/app/layout-20718f1503910291.js +1 -0
  178. package/temp/build/pi-desk/release/.next/static/chunks/app/login/page-0d3d4827802108e5.js +1 -0
  179. package/temp/build/pi-desk/release/.next/static/chunks/app/{not-found-2aab8b33ebbf3257.js → not-found-720cdf6a606b821a.js} +1 -1
  180. package/temp/build/pi-desk/release/.next/static/chunks/app/page-e8a5274c115f528d.js +158 -0
  181. package/temp/build/pi-desk/release/.next/static/chunks/app/project-preview/page-d70ee4e9409b970a.js +1 -0
  182. package/temp/build/pi-desk/release/.next/static/chunks/webpack-b60891a214e0925d.js +1 -0
  183. package/temp/build/pi-desk/release/.next/static/css/{250a6b93cc2222d1.css → 1e780a4936aa6ba3.css} +1 -1
  184. package/temp/build/pi-desk/release/.next/static/css/f646ca6c07245c4c.css +1 -0
  185. package/temp/build/pi-desk/release/.next/trace-build +1 -1
  186. package/temp/build/pi-desk/release/.next/server/chunks/10881.js +0 -296
  187. package/temp/build/pi-desk/release/.next/static/RLKcK-RQF5FvEeDmS9uih/_buildManifest.js +0 -1
  188. package/temp/build/pi-desk/release/.next/static/chunks/27611-0890482fe76bd9d6.js +0 -238
  189. package/temp/build/pi-desk/release/.next/static/chunks/31087-d8e5765d541d5c26.js +0 -1
  190. package/temp/build/pi-desk/release/.next/static/chunks/3814-35cfab0ce3aa7c41.js +0 -1
  191. package/temp/build/pi-desk/release/.next/static/chunks/70669.07f3670bfb7d6dd2.js +0 -1
  192. package/temp/build/pi-desk/release/.next/static/chunks/74220-795a2e7185bd66cb.js +0 -1
  193. package/temp/build/pi-desk/release/.next/static/chunks/77592-5bef6acb8e7c4b96.js +0 -13
  194. package/temp/build/pi-desk/release/.next/static/chunks/78923-17a9c337b29dcecd.js +0 -1
  195. package/temp/build/pi-desk/release/.next/static/chunks/81881-f11dbe7dc0d04ed9.js +0 -1
  196. package/temp/build/pi-desk/release/.next/static/chunks/96995-8f8313ea2ac3abd7.js +0 -62
  197. package/temp/build/pi-desk/release/.next/static/chunks/app/layout-a50c456648537afb.js +0 -3
  198. package/temp/build/pi-desk/release/.next/static/chunks/app/login/page-84892b06243891f0.js +0 -1
  199. package/temp/build/pi-desk/release/.next/static/chunks/app/page-0789c43551b29dd2.js +0 -158
  200. package/temp/build/pi-desk/release/.next/static/chunks/webpack-d7248206b7fc7ba8.js +0 -1
  201. package/temp/build/pi-desk/release/.next/static/css/a4cf28fd76c70165.css +0 -1
  202. /package/temp/build/pi-desk/release/.next/static/{RLKcK-RQF5FvEeDmS9uih → MV6_lBt7Mzmi1rBNI-3dx}/_ssgManifest.js +0 -0
@@ -7,7 +7,8 @@ import {
7
7
  clearL3ConversationTailSpacer,
8
8
  findL3ConversationMessage,
9
9
  logL3ConversationScroll,
10
- positionL3ConversationMessage
10
+ positionL3ConversationMessage,
11
+ refreshL3ConversationTailSpacer
11
12
  } from '@client/l3_modules/conversation/l3-conversation-viewport'
12
13
  import type { L2WorkbenchChatViewportSnapshot } from '../l2-workbench-chat'
13
14
 
@@ -252,25 +253,18 @@ export function L2WorkbenchChatViewportManager({
252
253
  const enterRunningAtBottom =
253
254
  entryPosition?.follow ?? (initializing && running && snapshot === null)
254
255
  const entryTarget = target
255
- const positionEntry = (): void => {
256
- stopScroll()
257
- if (entryTarget) {
258
- positionL3ConversationMessage(scrollElement, entryTarget, targetOffset, containsTail)
259
- } else {
260
- clearL3ConversationTailSpacer(scrollElement)
261
- }
262
- if (enterRunningAtBottom || !entryTarget) {
263
- // 首屏懒布局展开后也在绘制前置底,不交给后续缓动收敛。
264
- scrollElement.scrollTop = Math.max(
265
- 0,
266
- scrollElement.scrollHeight - scrollElement.clientHeight
267
- )
268
- }
269
- markProgrammaticScroll(state, scrollElement)
270
- // 同步确立跟随,立即切走也不能丢失这个状态。
271
- if (followAfterPosition || !entryTarget) void scrollToBottom({ animation: 'instant' })
256
+ stopScroll()
257
+ if (entryTarget) {
258
+ positionL3ConversationMessage(scrollElement, entryTarget, targetOffset, containsTail)
259
+ } else {
260
+ clearL3ConversationTailSpacer(scrollElement)
261
+ }
262
+ if (enterRunningAtBottom || !entryTarget) {
263
+ scrollElement.scrollTop = Math.max(0, state.calculatedTargetScrollTop)
272
264
  }
273
- positionEntry()
265
+ markProgrammaticScroll(state, scrollElement)
266
+ // 同步确立跟随,立即切走也不能丢失这个状态。
267
+ if (followAfterPosition || !entryTarget) void scrollToBottom({ animation: 'instant' })
274
268
 
275
269
  logL3ConversationScroll('viewport:initialize:positioned', scrollElement, {
276
270
  initializing,
@@ -288,7 +282,18 @@ export function L2WorkbenchChatViewportManager({
288
282
  anchor: anchor ? { messageId: anchor, offset: targetOffset } : null,
289
283
  follow: followAfterPosition || target === null
290
284
  }
291
- const observer = new ResizeObserver(positionEntry)
285
+ const observer = new ResizeObserver(() => {
286
+ // 懒布局与拖宽只校正既定目标,不再重走提问定位、停止和启动跟随。
287
+ if (followAfterPosition || !entryTarget) {
288
+ refreshL3ConversationTailSpacer(scrollElement)
289
+ scrollElement.scrollTop = Math.max(0, state.calculatedTargetScrollTop)
290
+ } else {
291
+ // 内容收缩时库可能自动恢复跟随;首屏阅读锚点仍优先。
292
+ if (state.isAtBottom) stopScroll()
293
+ positionL3ConversationMessage(scrollElement, entryTarget, targetOffset, containsTail)
294
+ }
295
+ markProgrammaticScroll(state, scrollElement)
296
+ })
292
297
  observer.observe(scrollElement)
293
298
  if (scrollElement.firstElementChild) observer.observe(scrollElement.firstElementChild)
294
299
  const interactionElement = scrollElement.parentElement!
@@ -356,13 +361,15 @@ export function L2WorkbenchChatViewportManager({
356
361
  isAtBottom: state.isAtBottom,
357
362
  escapedFromLock: state.escapedFromLock
358
363
  })
364
+ // 新视图会在旧视图清理前读取快照,恢复跟随时就释放旧阅读位置。
365
+ onClearSnapshot()
359
366
  void scrollToBottom({ animation: 'instant' })
360
367
  }
361
368
  }
362
369
 
363
370
  scrollElement.addEventListener('scroll', resumeFollowAtBottom, { passive: true })
364
371
  return () => scrollElement.removeEventListener('scroll', resumeFollowAtBottom)
365
- }, [latestUserAnchor, scrollRef, scrollToBottom, state, visible])
372
+ }, [latestUserAnchor, onClearSnapshot, scrollRef, scrollToBottom, state, visible])
366
373
 
367
374
  useLayoutEffect(() => {
368
375
  if (!running || !visible) return
@@ -1,3 +1,35 @@
1
+ .chatContainer {
2
+ container-name: work-session-chat;
3
+ container-type: inline-size;
4
+ }
5
+
6
+ .headerActionIcon {
7
+ display: none;
8
+ }
9
+
10
+ @container work-session-chat (width < 28rem) {
11
+ .sessionHeader {
12
+ gap: 0.5rem;
13
+ }
14
+
15
+ .headerAction {
16
+ width: 1.75rem;
17
+ height: 1.75rem;
18
+ min-height: 1.75rem;
19
+ gap: 0;
20
+ border-radius: 0.375rem;
21
+ padding: 0;
22
+ }
23
+
24
+ .headerActionLabel {
25
+ display: none;
26
+ }
27
+
28
+ .headerActionIcon {
29
+ display: block;
30
+ }
31
+ }
32
+
1
33
  .collapsedText {
2
34
  overflow: hidden;
3
35
  text-overflow: ellipsis;
@@ -54,8 +54,10 @@ import { useL2WorkSessionColumnCapacity } from './hooks/l2-work-session-column-c
54
54
  import type { L2FileWorkspaceRuntime } from './file-workspace/l2-file-workspace-runtime'
55
55
  import { L2WorkSessionFileLayout } from './file-workspace/l2-work-session-file-layout'
56
56
  import { Button, buttonVariants } from '@client/l4_foundation/ui/shadcn/button'
57
+ import type { L2WorkbenchPluginSessionSlots } from './l2-workbench-plugin-slots'
57
58
 
58
- interface L2WorkSessionColumnsProps extends L2WorkbenchComposerSlots {
59
+ interface L2WorkSessionColumnsProps
60
+ extends L2WorkbenchComposerSlots, L2WorkbenchPluginSessionSlots {
59
61
  onOpenSettings?: (target?: 'model-presets') => void
60
62
  workSessions: readonly L2WorkSessionListItem[]
61
63
  primaryWorkId: string | null
@@ -126,7 +128,7 @@ function SidebarToggle({
126
128
  )
127
129
  }
128
130
 
129
- interface WorkSessionColumnProps extends L2WorkbenchComposerSlots {
131
+ interface WorkSessionColumnProps extends L2WorkbenchComposerSlots, L2WorkbenchPluginSessionSlots {
130
132
  onOpenSettings?: (target?: 'model-presets') => void
131
133
  workSession: L2WorkSessionListItem
132
134
  fixed: boolean
@@ -181,6 +183,8 @@ const WorkSessionColumn = memo(function WorkSessionColumn({
181
183
  renderTaskSummary,
182
184
  renderTaskMenu,
183
185
  renderPluginComposerPanel,
186
+ renderPluginWidgets,
187
+ renderPluginSessionHeader,
184
188
  gitRuntime,
185
189
  invokePluginMethod,
186
190
  listPiCommands,
@@ -214,6 +218,34 @@ const WorkSessionColumn = memo(function WorkSessionColumn({
214
218
  useSyncExternalStore(fileRuntime.subscribe, fileRuntime.getRevision, fileRuntime.getRevision)
215
219
  const fileWorkspace = fileRuntime.getWorkspace(workSession.workId)
216
220
  const fileCount = (fileWorkspace?.tabs.length ?? 0) + (fileWorkspace?.activeDiff ? 1 : 0)
221
+ const source = useMemo<L2ChatSource>(
222
+ () => ({
223
+ workId: workSession.workId,
224
+ sessionId: workSession.sessionId,
225
+ branchId: workSession.branchId
226
+ }),
227
+ [workSession.workId, workSession.sessionId, workSession.branchId]
228
+ )
229
+ const title = (
230
+ <div className="min-w-0">
231
+ <p
232
+ className={cn(
233
+ 'truncate text-sm transition-colors duration-150 motion-reduce:transition-none',
234
+ focused ? 'font-semibold text-foreground' : 'font-medium text-muted-foreground'
235
+ )}
236
+ title={workSession.projectName}
237
+ >
238
+ {workSession.projectName}
239
+ </p>
240
+ <p
241
+ className="truncate text-xs text-muted-foreground"
242
+ data-testid="session-header-title"
243
+ title={workSession.sessionTitle ?? t('newSession')}
244
+ >
245
+ {workSession.sessionTitle ?? t('newSession')}
246
+ </p>
247
+ </div>
248
+ )
217
249
 
218
250
  useEffect(() => {
219
251
  const column = columnRef.current
@@ -259,23 +291,18 @@ const WorkSessionColumn = memo(function WorkSessionColumn({
259
291
  onOpenSidebar={() => onOpenFileSidebar(workSession.workId)}
260
292
  >
261
293
  {(chatVisible) => (
262
- <div className="flex h-full min-h-0 min-w-0 flex-1 flex-col">
263
- <header className="flex min-h-14 items-center gap-3 px-3">
294
+ <div className={cn(styles.chatContainer, 'flex h-full min-h-0 min-w-0 flex-1 flex-col')}>
295
+ <header
296
+ className={cn(styles.sessionHeader, 'flex min-h-14 shrink-0 items-center gap-3 px-3')}
297
+ data-testid="session-header"
298
+ >
264
299
  {fixed && !mobile ? null : (
265
300
  <SidebarToggle sidebarOpen={sidebarOpen} onToggle={onToggleSidebar} />
266
301
  )}
267
302
  <div className="min-w-0 flex-1">
268
- <p
269
- className={cn(
270
- 'truncate text-sm transition-colors duration-150 motion-reduce:transition-none',
271
- focused ? 'font-semibold text-foreground' : 'font-medium text-muted-foreground'
272
- )}
273
- >
274
- {workSession.projectName}
275
- </p>
276
- <p className="truncate text-xs text-muted-foreground">
277
- {workSession.sessionTitle ?? t('newSession')}
278
- </p>
303
+ {chatVisible && contentActive
304
+ ? renderPluginSessionHeader({ source, title })
305
+ : title}
279
306
  </div>
280
307
  <div className="-mr-1 flex shrink-0 items-center">
281
308
  {initializationIssue.error ? (
@@ -292,13 +319,17 @@ const WorkSessionColumn = memo(function WorkSessionColumn({
292
319
  {fileCount > 0 ? (
293
320
  <Button
294
321
  variant="ghost"
322
+ className={styles.headerAction}
295
323
  data-testid="session-files-entry"
296
324
  aria-label={t('openSessionFiles')}
325
+ title={t('sessionFiles', { count: fileCount })}
297
326
  aria-pressed={fileWorkspace?.windowOpen ?? false}
298
327
  onClick={() => fileRuntime.showWindow(workSession.workId)}
299
328
  >
300
329
  <FilesIcon className="size-4" />
301
- {t('sessionFiles', { count: fileCount })}
330
+ <span className={styles.headerActionLabel}>
331
+ {t('sessionFiles', { count: fileCount })}
332
+ </span>
302
333
  </Button>
303
334
  ) : null}
304
335
  {onUnpinWorkSession ? (
@@ -338,6 +369,8 @@ const WorkSessionColumn = memo(function WorkSessionColumn({
338
369
  renderTaskSummary={renderTaskSummary}
339
370
  renderTaskMenu={renderTaskMenu}
340
371
  renderPluginComposerPanel={renderPluginComposerPanel}
372
+ renderPluginWidgets={renderPluginWidgets}
373
+ widgetsVisible={chatVisible && contentActive}
341
374
  gitRuntime={gitRuntime}
342
375
  active={composerActive}
343
376
  mobile={mobile}
@@ -473,6 +506,8 @@ export function L2WorkSessionColumns({
473
506
  renderTaskSummary,
474
507
  renderTaskMenu,
475
508
  renderPluginComposerPanel,
509
+ renderPluginWidgets,
510
+ renderPluginSessionHeader,
476
511
  gitRuntime,
477
512
  invokePluginMethod,
478
513
  listPiCommands,
@@ -660,6 +695,8 @@ export function L2WorkSessionColumns({
660
695
  renderTaskSummary={renderTaskSummary}
661
696
  renderTaskMenu={renderTaskMenu}
662
697
  renderPluginComposerPanel={renderPluginComposerPanel}
698
+ renderPluginWidgets={renderPluginWidgets}
699
+ renderPluginSessionHeader={renderPluginSessionHeader}
663
700
  gitRuntime={gitRuntime}
664
701
  invokePluginMethod={invokePluginMethod}
665
702
  listPiCommands={listPiCommands}
@@ -758,6 +795,8 @@ export function L2WorkSessionColumns({
758
795
  renderTaskSummary={renderTaskSummary}
759
796
  renderTaskMenu={renderTaskMenu}
760
797
  renderPluginComposerPanel={renderPluginComposerPanel}
798
+ renderPluginWidgets={renderPluginWidgets}
799
+ renderPluginSessionHeader={renderPluginSessionHeader}
761
800
  gitRuntime={gitRuntime}
762
801
  invokePluginMethod={invokePluginMethod}
763
802
  listPiCommands={listPiCommands}
@@ -21,6 +21,7 @@ import {
21
21
  EllipsisIcon,
22
22
  FolderTreeIcon,
23
23
  FolderSearchIcon,
24
+ ExternalLinkIcon,
24
25
  LoaderCircleIcon,
25
26
  MessageSquareTextIcon,
26
27
  PinIcon,
@@ -131,6 +132,7 @@ interface L2WorkSessionSidebarProps {
131
132
  onModeChange: (mode: L2WorkSessionSidebarMode) => void
132
133
  onRefreshFiles: () => void
133
134
  onPreviewProject: () => void
135
+ onPreviewProjectWindow: () => void
134
136
  onOpenTerminal: () => void
135
137
  onCreate: () => void
136
138
  onReplace: () => void
@@ -680,6 +682,7 @@ function L2WorkSessionSidebarComponent({
680
682
  onModeChange,
681
683
  onRefreshFiles,
682
684
  onPreviewProject,
685
+ onPreviewProjectWindow,
683
686
  onOpenTerminal,
684
687
  onCreate,
685
688
  onReplace,
@@ -769,14 +772,6 @@ function L2WorkSessionSidebarComponent({
769
772
  ]
770
773
  : mode === 'files'
771
774
  ? [
772
- {
773
- key: 'new-terminal',
774
- label: t('newTerminal'),
775
- disabled,
776
- icon: <TerminalIcon />,
777
- testId: 'project-files-terminal-open',
778
- onSelect: onOpenTerminal
779
- },
780
775
  {
781
776
  key: 'preview-project',
782
777
  label: t('projectPreview'),
@@ -785,6 +780,22 @@ function L2WorkSessionSidebarComponent({
785
780
  testId: 'project-preview-open',
786
781
  onSelect: onPreviewProject
787
782
  },
783
+ {
784
+ key: 'preview-project-window',
785
+ label: t('projectPreviewWindow'),
786
+ disabled: !canBrowseFiles,
787
+ icon: <ExternalLinkIcon />,
788
+ testId: 'project-preview-window-open',
789
+ onSelect: onPreviewProjectWindow
790
+ },
791
+ {
792
+ key: 'new-terminal',
793
+ label: t('newTerminal'),
794
+ disabled,
795
+ icon: <TerminalIcon />,
796
+ testId: 'project-files-terminal-open',
797
+ onSelect: onOpenTerminal
798
+ },
788
799
  {
789
800
  key: 'refresh-files',
790
801
  label: t('refreshFiles'),
@@ -89,6 +89,7 @@ import type { L3CapabilityModeNames } from '@common/l3_modules/capability-modes/
89
89
  import { L2WorkbenchCapabilityModePicker } from './l2-workbench-capability-mode-picker'
90
90
  import { useL2NativeUi } from './native-ui/l2-native-ui'
91
91
  import { L2NativeUiExtras, L2NativeUiQuestions } from './native-ui/l2-native-ui-view'
92
+ import type { L2WorkbenchPluginWidgetSlots } from './l2-workbench-plugin-slots'
92
93
 
93
94
  interface L2WorkbenchTaskSlotInput {
94
95
  source: L2ChatSource
@@ -107,13 +108,15 @@ export interface L2WorkbenchComposerSlots {
107
108
  }) => ReactNode
108
109
  }
109
110
 
110
- interface L2WorkbenchChatComposerProps extends L2WorkbenchComposerSlots {
111
+ interface L2WorkbenchChatComposerProps
112
+ extends L2WorkbenchComposerSlots, L2WorkbenchPluginWidgetSlots {
111
113
  onOpenSettings?: (target?: 'model-presets') => void
112
114
  workSession: L2WorkSessionListItem
113
115
  chatRuntime: L2WorkbenchChatRuntime
114
116
  inputRuntime: L2WorkbenchChatInputRuntime
115
117
  gitRuntime: L2GitWorkspaceRuntime
116
118
  active: boolean
119
+ widgetsVisible: boolean
117
120
  mobile: boolean
118
121
  invokePluginMethod: L2WorkbenchBiz['invokePluginMethod']
119
122
  listPiCommands: L2WorkbenchBiz['listPiCommands']
@@ -161,8 +164,10 @@ function L2WorkbenchChatComposerComponent({
161
164
  renderTaskSummary,
162
165
  renderTaskMenu,
163
166
  renderPluginComposerPanel,
167
+ renderPluginWidgets,
164
168
  gitRuntime,
165
169
  active,
170
+ widgetsVisible,
166
171
  mobile,
167
172
  invokePluginMethod,
168
173
  listPiCommands,
@@ -633,6 +638,9 @@ function L2WorkbenchChatComposerComponent({
633
638
  <p className="mb-1 px-1 text-xs text-muted-foreground">{t('nativeUiComposer')}</p>
634
639
  ) : null}
635
640
 
641
+ {widgetsVisible && !basicChat
642
+ ? renderPluginWidgets({ source, placement: 'aboveEditor' })
643
+ : null}
636
644
  <form
637
645
  aria-label={
638
646
  workSessionRunning
@@ -1021,6 +1029,9 @@ function L2WorkbenchChatComposerComponent({
1021
1029
  </PromptInputFooter>
1022
1030
  </InputGroup>
1023
1031
  </form>
1032
+ {widgetsVisible && !basicChat
1033
+ ? renderPluginWidgets({ source, placement: 'belowEditor' })
1034
+ : null}
1024
1035
  <L2NativeUiExtras snapshot={nativeUi.state.snapshot} placement="belowEditor" />
1025
1036
 
1026
1037
  {activePluginPanel &&
@@ -1,7 +1,9 @@
1
1
  'use client'
2
2
 
3
- import { CheckIcon, CopyIcon, LoaderCircleIcon, RefreshCwIcon } from 'lucide-react'
3
+ import { CheckIcon, CircleAlertIcon, CopyIcon, LoaderCircleIcon, RefreshCwIcon } from 'lucide-react'
4
4
  import { useTranslation } from 'react-i18next'
5
+ import { cn } from '@client/l4_foundation/lib/l4-utils'
6
+ import styles from './l2-work-session-columns.module.css'
5
7
  import { Button } from '@client/l4_foundation/ui/shadcn/button'
6
8
  import {
7
9
  Popover,
@@ -36,11 +38,14 @@ export function L2WorkbenchInitializationNotice({
36
38
  <Button
37
39
  variant="ghost"
38
40
  size="sm"
39
- className="text-xs text-destructive hover:text-destructive"
41
+ aria-label={t('initializationDegraded')}
42
+ title={t('initializationDegraded')}
43
+ className={cn(styles.headerAction, 'text-xs text-destructive hover:text-destructive')}
40
44
  />
41
45
  }
42
46
  >
43
- {t('initializationDegraded')}
47
+ <CircleAlertIcon className={cn(styles.headerActionIcon, 'size-4')} aria-hidden="true" />
48
+ <span className={styles.headerActionLabel}>{t('initializationDegraded')}</span>
44
49
  </PopoverTrigger>
45
50
  <PopoverContent
46
51
  align="end"
@@ -0,0 +1,13 @@
1
+ import type { ReactNode } from 'react'
2
+ import type { PluginSource } from '@jetcrab/pi-desk-sdk/browser'
3
+
4
+ export interface L2WorkbenchPluginWidgetSlots {
5
+ renderPluginWidgets: (input: {
6
+ source: PluginSource
7
+ placement: 'aboveEditor' | 'belowEditor'
8
+ }) => ReactNode
9
+ }
10
+
11
+ export interface L2WorkbenchPluginSessionSlots extends L2WorkbenchPluginWidgetSlots {
12
+ renderPluginSessionHeader: (input: { source: PluginSource; title: ReactNode }) => ReactNode
13
+ }
@@ -66,6 +66,7 @@ import { L4_APP_VERSION } from '@client/l4_foundation/l4-app-info'
66
66
  import { L2WorkbenchSidebarLayout } from './l2-workbench-sidebar-layout'
67
67
  import type { L4PluginBrowserContributionDescriptor } from '@client/l4_foundation/plugin-host/l4-plugin-host-runtime'
68
68
  import type { L2WorkbenchComposerSlots } from './l2-workbench-chat-composer'
69
+ import type { L2WorkbenchPluginSessionSlots } from './l2-workbench-plugin-slots'
69
70
  import { createL2WorkbenchBiz } from './l2-workbench-biz'
70
71
  import { createL2WorkbenchVersionRefresh } from './l2-workbench-version-refresh'
71
72
  import { L2FileWorkspaceRuntime } from './file-workspace/l2-file-workspace-runtime'
@@ -172,10 +173,11 @@ function WorkbenchDisplaySlot<Input>({
172
173
  return render(input)
173
174
  }
174
175
 
175
- interface L2WorkbenchProps extends L2WorkbenchComposerSlots {
176
+ interface L2WorkbenchProps extends L2WorkbenchComposerSlots, L2WorkbenchPluginSessionSlots {
176
177
  onTerminalReady: () => Promise<void>
177
178
  onTerminalDisconnected: () => void
178
179
  onOpenTerminal: (cwd?: string) => Promise<void>
180
+ onOpenProjectPreviewWindow: (cwd: string) => void
179
181
  renderTerminalWorkspace: (input: { mobile: boolean; children: ReactNode }) => ReactNode
180
182
  listPluginManagement: () => Promise<L2PluginManagementSnapshot>
181
183
  subscribePluginManagementChanges: (listener: () => void) => () => void
@@ -221,6 +223,7 @@ export function L2Workbench({
221
223
  onTerminalReady,
222
224
  onTerminalDisconnected,
223
225
  onOpenTerminal,
226
+ onOpenProjectPreviewWindow,
224
227
  renderTerminalWorkspace,
225
228
  listPluginManagement,
226
229
  subscribePluginManagementChanges,
@@ -234,7 +237,9 @@ export function L2Workbench({
234
237
  renderSettings,
235
238
  renderTaskSummary,
236
239
  renderTaskMenu,
237
- renderPluginComposerPanel
240
+ renderPluginComposerPanel,
241
+ renderPluginWidgets,
242
+ renderPluginSessionHeader
238
243
  }: L2WorkbenchProps): React.JSX.Element {
239
244
  const { t } = useTranslation('workbench')
240
245
  const { clientId, appSocket } = useL4AppSocket()
@@ -1917,6 +1922,12 @@ export function L2Workbench({
1917
1922
  fileRuntime.collapseExpandedWindow()
1918
1923
  pluginHost.previewProject(activeWorkId ? fileRuntime.getDirectory(activeWorkId) : activeCwd)
1919
1924
  }}
1925
+ onPreviewProjectWindow={() => {
1926
+ if (!activeCwd) return
1927
+ onOpenProjectPreviewWindow(
1928
+ activeWorkId ? fileRuntime.getDirectory(activeWorkId) : activeCwd
1929
+ )
1930
+ }}
1920
1931
  onOpenTerminal={() => {
1921
1932
  closeMobileDrawer()
1922
1933
  fileRuntime.collapseExpandedWindow()
@@ -1973,6 +1984,8 @@ export function L2Workbench({
1973
1984
  renderTaskSummary={renderTaskSummary}
1974
1985
  renderTaskMenu={renderTaskMenu}
1975
1986
  renderPluginComposerPanel={renderPluginComposerPanel}
1987
+ renderPluginWidgets={renderPluginWidgets}
1988
+ renderPluginSessionHeader={renderPluginSessionHeader}
1976
1989
  gitRuntime={gitRuntime}
1977
1990
  invokePluginMethod={workbenchBiz.invokePluginMethod}
1978
1991
  listPiCommands={workbenchBiz.listPiCommands}
@@ -157,9 +157,11 @@ export function positionL3ConversationMessage(
157
157
  offset,
158
158
  addTailSpace
159
159
  })
160
- clearL3ConversationTailSpacer(scrollElement)
161
-
162
160
  const turn = addTailSpace ? logicalTailTurn(scrollElement) : null
161
+ // 同一轮的定位空间可直接重算,先清空会让浏览器截短 scrollTop。
162
+ if (!turn || activeTailTurn(scrollElement) !== turn) {
163
+ clearL3ConversationTailSpacer(scrollElement)
164
+ }
163
165
  if (!turn) {
164
166
  scrollElement.scrollTop = Math.max(
165
167
  0,
@@ -177,11 +179,15 @@ export function positionL3ConversationMessage(
177
179
  }
178
180
 
179
181
  const { targetScrollTop, minHeight } = measureTailPosition(scrollElement, target, turn, offset)
180
- turn.style.minHeight = `${Math.ceil(minHeight)}px`
181
- turn.dataset.chatTailSpace = ''
182
+ const nextMinHeight = `${Math.ceil(minHeight)}px`
183
+ if (turn.style.minHeight !== nextMinHeight) turn.style.minHeight = nextMinHeight
184
+ if (!turn.hasAttribute('data-chat-tail-space')) turn.dataset.chatTailSpace = ''
182
185
  const anchor = target.dataset.chatScrollAnchor
183
- if (anchor) turn.dataset.chatTailAnchor = anchor
184
- turn.dataset.chatTailOffset = String(offset)
186
+ if (turn.dataset.chatTailAnchor !== anchor) {
187
+ if (anchor) turn.dataset.chatTailAnchor = anchor
188
+ else delete turn.dataset.chatTailAnchor
189
+ }
190
+ if (turn.dataset.chatTailOffset !== String(offset)) turn.dataset.chatTailOffset = String(offset)
185
191
  scrollElement.scrollTop = targetScrollTop
186
192
  logL3ConversationScroll('position:end', scrollElement, {
187
193
  target: target.dataset.chatScrollAnchor ?? null,
@@ -1,7 +1,7 @@
1
1
  .document {
2
2
  box-sizing: border-box;
3
3
  width: 100%;
4
- max-width: 80ch;
4
+ max-width: 1200px;
5
5
  min-width: 0;
6
6
  margin-inline: auto;
7
7
  padding: 1.5rem;
@@ -10,7 +10,8 @@ export function useL3ProjectDocumentGit(
10
10
  git: L3ProjectGitBrowser,
11
11
  host: RefObject<HTMLDivElement | null>,
12
12
  savePosition: () => void,
13
- active: boolean
13
+ active: boolean,
14
+ refreshOnFocus = true
14
15
  ): { refresh: () => void; unavailable: boolean } {
15
16
  useSyncExternalStore(git.subscribe, git.getRevision, git.getRevision)
16
17
  const snapshot = git.getWorkspace(runtime.cwd)?.snapshot
@@ -29,7 +30,7 @@ export function useL3ProjectDocumentGit(
29
30
  }, [diffActive, git, path, runtime])
30
31
 
31
32
  useEffect(() => {
32
- if (!active || !path || diffActive) return
33
+ if (!refreshOnFocus || !active || !path || diffActive) return
33
34
  const visible = (): boolean =>
34
35
  window.document.visibilityState === 'visible' &&
35
36
  Boolean(host.current?.getClientRects().length) &&
@@ -54,7 +55,7 @@ export function useL3ProjectDocumentGit(
54
55
  window.removeEventListener('focus', onFocus)
55
56
  window.document.removeEventListener('visibilitychange', onFocus)
56
57
  }
57
- }, [active, diffActive, git, host, path, refresh, runtime])
58
+ }, [active, diffActive, git, host, path, refresh, refreshOnFocus, runtime])
58
59
 
59
60
  useEffect(() => {
60
61
  if (!active || !path || diffActive || document?.status !== 'text') return
@@ -53,6 +53,7 @@ export function L3ProjectDocumentView({
53
53
  gitRuntime,
54
54
  compact,
55
55
  visible = true,
56
+ refreshOnFocus = true,
56
57
  onEscape,
57
58
  onSwitchPane
58
59
  }: {
@@ -61,6 +62,7 @@ export function L3ProjectDocumentView({
61
62
  gitRuntime: L3ProjectGitBrowser
62
63
  compact: boolean
63
64
  visible?: boolean
65
+ refreshOnFocus?: boolean
64
66
  onEscape: () => void
65
67
  onSwitchPane?: () => void
66
68
  }): React.JSX.Element {
@@ -107,7 +109,8 @@ export function L3ProjectDocumentView({
107
109
  gitRuntime,
108
110
  hostRef,
109
111
  savePosition,
110
- visible
112
+ visible,
113
+ refreshOnFocus
111
114
  )
112
115
  const comparison = document?.status === 'text' ? document.comparison : undefined
113
116
  const [computed, setComputed] = useState<{
@@ -85,49 +85,37 @@ export interface L3ProjectReader extends L3ProjectFileReader {
85
85
 
86
86
  export function createL3ProjectReader(
87
87
  clientId: string,
88
- context: contract.L3ProjectReadContext
88
+ context: contract.L3ProjectReadContext,
89
+ onUnauthorized?: () => void
89
90
  ): L3ProjectReader {
91
+ async function request<I, O>(
92
+ api: { path: string; request: z.ZodType<I>; response: z.ZodType<O> },
93
+ input: I,
94
+ signal?: AbortSignal
95
+ ): Promise<O> {
96
+ try {
97
+ return await requestL3Project(clientId, api, input, signal)
98
+ } catch (cause) {
99
+ if (!signal?.aborted && cause instanceof L3ProjectRequestError && cause.status === 401)
100
+ onUnauthorized?.()
101
+ throw cause
102
+ }
103
+ }
90
104
  return {
91
- list: (path, signal) =>
92
- requestL3Project(clientId, contract.L3ProjectFileList, { ...context, path }, signal),
105
+ list: (path, signal) => request(contract.L3ProjectFileList, { ...context, path }, signal),
93
106
  search: (input, signal) =>
94
- requestL3Project(clientId, contract.L3ProjectFileSearch, { ...context, ...input }, signal),
107
+ request(contract.L3ProjectFileSearch, { ...context, ...input }, signal),
95
108
  get: (path, imagePreviewMode, signal) =>
96
- requestL3Project(
97
- clientId,
98
- contract.L3ProjectFileGet,
99
- { ...context, path, imagePreviewMode },
100
- signal
101
- ),
109
+ request(contract.L3ProjectFileGet, { ...context, path, imagePreviewMode }, signal),
102
110
  snapshot: (signal, refresh) =>
103
- requestL3Project(clientId, contract.L3ProjectGitGet, { ...context, refresh }, signal),
111
+ request(contract.L3ProjectGitGet, { ...context, refresh }, signal),
104
112
  branches: (repositoryRoot, signal) =>
105
- requestL3Project(
106
- clientId,
107
- contract.L3ProjectGitBranches,
108
- { ...context, repositoryRoot },
109
- signal
110
- ),
113
+ request(contract.L3ProjectGitBranches, { ...context, repositoryRoot }, signal),
111
114
  log: (repositoryRoot, query, signal) =>
112
- requestL3Project(
113
- clientId,
114
- contract.L3ProjectGitLog,
115
- { ...context, repositoryRoot, ...query },
116
- signal
117
- ),
115
+ request(contract.L3ProjectGitLog, { ...context, repositoryRoot, ...query }, signal),
118
116
  changes: (repositoryRoot, query, signal) =>
119
- requestL3Project(
120
- clientId,
121
- contract.L3ProjectGitChanges,
122
- { ...context, repositoryRoot, ...query },
123
- signal
124
- ),
117
+ request(contract.L3ProjectGitChanges, { ...context, repositoryRoot, ...query }, signal),
125
118
  diff: (repositoryRoot, path, signal, comparison) =>
126
- requestL3Project(
127
- clientId,
128
- contract.L3ProjectGitDiff,
129
- { ...context, repositoryRoot, path, comparison },
130
- signal
131
- )
119
+ request(contract.L3ProjectGitDiff, { ...context, repositoryRoot, path, comparison }, signal)
132
120
  }
133
121
  }
@@ -0,0 +1,11 @@
1
+ 'use client'
2
+
3
+ import { createContext, useContext } from 'react'
4
+
5
+ export const L4ClientIdContext = createContext<string | null>(null)
6
+
7
+ export function useL4ClientId(): string {
8
+ const clientId = useContext(L4ClientIdContext)
9
+ if (clientId === null) throw new Error('缺少页面 clientId Provider')
10
+ return clientId
11
+ }