@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
@@ -0,0 +1,342 @@
1
+ 'use client'
2
+
3
+ import type { ReactNode } from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+ import {
6
+ Columns2Icon,
7
+ FileCode2Icon,
8
+ FolderTreeIcon,
9
+ GitBranchIcon,
10
+ Maximize2Icon,
11
+ Minimize2Icon,
12
+ PanelBottomCloseIcon,
13
+ PanelLeftCloseIcon,
14
+ PanelLeftOpenIcon,
15
+ RefreshCwIcon
16
+ } from 'lucide-react'
17
+ import {
18
+ ResizablePanelGroup,
19
+ ResizablePanel,
20
+ ResizableHandle
21
+ } from '@client/l4_foundation/ui/shadcn/resizable'
22
+ import { Button } from '@client/l4_foundation/ui/shadcn/button'
23
+ import { L3ProjectFileBrowser } from '@client/l3_modules/project-files/l3-project-file-browser'
24
+ import { L3ProjectFileTabs } from '@client/l3_modules/project-files/l3-project-file-tabs'
25
+ import { L3ProjectDocumentView } from '@client/l3_modules/project-files/l3-project-document-view'
26
+ import { useL4AppToast } from '@client/l4_foundation/ui/l4-app-toast'
27
+ import { copyL4BrowserText } from '@client/l4_foundation/lib/l4-browser-clipboard'
28
+ import { useL2ProjectPreview } from './hooks/l2-use-project-preview'
29
+ import { L2ProjectHistoryView } from './views/l2-project-history-view'
30
+
31
+ const ignoreEscape = (): void => {}
32
+
33
+ export function L2ProjectPreview({
34
+ clientId,
35
+ cwd,
36
+ refreshOnFocus,
37
+ title,
38
+ actions,
39
+ onEscape = ignoreEscape,
40
+ onUnauthorized
41
+ }: {
42
+ clientId: string
43
+ cwd: string
44
+ refreshOnFocus: boolean
45
+ title?: ReactNode
46
+ actions?: ReactNode
47
+ onEscape?: () => void
48
+ onUnauthorized?: () => void
49
+ }): React.JSX.Element {
50
+ const { t } = useTranslation('projectHistory')
51
+ const toast = useL4AppToast()
52
+ const {
53
+ files,
54
+ git,
55
+ history,
56
+ global,
57
+ updateGlobal,
58
+ page,
59
+ setPage,
60
+ historyPane,
61
+ setHistoryPane,
62
+ setContainer,
63
+ narrow,
64
+ stacked,
65
+ width,
66
+ openGit,
67
+ refresh
68
+ } = useL2ProjectPreview(clientId, cwd, refreshOnFocus, onUnauthorized)
69
+ const { directoryOpen, gitOpen, gitMaximized } = global
70
+ const projectName = cwd.replaceAll('\\', '/').split('/').filter(Boolean).at(-1) ?? cwd
71
+ const gitState = git.getWorkspace(cwd)!
72
+ const repositories = gitState.snapshot.repositories
73
+ const state = files.state
74
+ const diffPath = state.activeDiff
75
+ ? [state.activeDiff.repositoryRoot, state.activeDiff.path].filter(Boolean).join('/')
76
+ : null
77
+ const fileTree = (
78
+ <L3ProjectFileBrowser
79
+ cwd={cwd}
80
+ projectName={projectName}
81
+ runtime={files}
82
+ gitRuntime={git}
83
+ onOpenGitBranches={openGit}
84
+ onFileOpened={() => setPage('code')}
85
+ />
86
+ )
87
+ const code = (
88
+ <div className="flex size-full min-h-0 min-w-0 flex-col">
89
+ <L3ProjectFileTabs
90
+ cwd={cwd}
91
+ gitRuntime={git}
92
+ paths={state.tabs}
93
+ activePath={state.activePath}
94
+ diffPath={diffPath}
95
+ mobile={narrow}
96
+ onActivate={(path) => files.openFile(path)}
97
+ onClose={(path, scope) => files.closeTabs(path, scope)}
98
+ />
99
+ <L3ProjectDocumentView
100
+ runtime={files}
101
+ gitRuntime={git}
102
+ compact={width < 1100}
103
+ refreshOnFocus={refreshOnFocus}
104
+ onEscape={onEscape}
105
+ />
106
+ </div>
107
+ )
108
+ const gitView = (
109
+ <div className="flex size-full min-h-0 min-w-0 flex-col">
110
+ <div className="flex h-9 shrink-0 items-center gap-2 border-b bg-muted px-3">
111
+ <GitBranchIcon className="size-4" />
112
+ <span className="flex-1 text-sm font-medium">Git</span>
113
+ {!stacked ? (
114
+ <Button
115
+ size="icon-sm"
116
+ variant="ghost"
117
+ aria-label={t(gitMaximized ? 'restoreGit' : 'maximizeGit')}
118
+ onClick={() => updateGlobal({ gitMaximized: !gitMaximized })}
119
+ >
120
+ {gitMaximized ? (
121
+ <Minimize2Icon className="size-4" />
122
+ ) : (
123
+ <Maximize2Icon className="size-4" />
124
+ )}
125
+ </Button>
126
+ ) : null}
127
+ <Button
128
+ size="icon-sm"
129
+ variant="ghost"
130
+ aria-label={t('collapseGit')}
131
+ onClick={() => {
132
+ updateGlobal({ gitOpen: false, gitMaximized: false })
133
+ setPage('code')
134
+ }}
135
+ >
136
+ <PanelBottomCloseIcon className="size-4" />
137
+ </Button>
138
+ </div>
139
+ {repositories.length ? (
140
+ <L2ProjectHistoryView
141
+ state={history.state}
142
+ cwd={cwd}
143
+ changeRows={history.getChangeRows()}
144
+ onSelectChange={(path) => history.selectChange(path)}
145
+ onToggleDirectory={(path) => history.toggleChangeDirectory(path)}
146
+ onReveal={(path, type) => files.revealFile(path, type)}
147
+ repositories={repositories}
148
+ projectName={projectName}
149
+ narrow={narrow}
150
+ mobilePane={historyPane}
151
+ onMobilePane={setHistoryPane}
152
+ scroll={history.scroll}
153
+ onScroll={(pane, value) => history.setScroll(pane, value)}
154
+ panelSizes={{ log: global.logSize, changes: global.changeSize }}
155
+ onPanelSize={(panel, value) =>
156
+ updateGlobal(panel === 'log' ? { logSize: value } : { changeSize: value })
157
+ }
158
+ onRepository={(root) => {
159
+ history.setRepository(root)
160
+ setHistoryPane('log')
161
+ }}
162
+ onMode={(mode) => history.setMode(mode)}
163
+ onRevision={(field, value) => history.setRevision(field, value)}
164
+ onStrategy={(strategy) => history.setStrategy(strategy)}
165
+ onQuery={(query) => history.setQuery(query)}
166
+ onAuthor={(author) => history.setAuthor(author)}
167
+ onCommit={(oid, parent) => history.selectCommit(oid, parent)}
168
+ onAggregate={() => history.showAggregate()}
169
+ onMoreLog={() => history.loadMoreLog()}
170
+ onMoreChanges={() => history.loadMoreChanges()}
171
+ onOpenFile={(path) => {
172
+ history.openChange(path)
173
+ setPage('code')
174
+ updateGlobal({ gitMaximized: false })
175
+ }}
176
+ onRefresh={() => history.refresh()}
177
+ onCopy={(text) => {
178
+ void copyL4BrowserText(text).then((ok) => {
179
+ if (!ok) toast.error(t('copyFailed'))
180
+ })
181
+ }}
182
+ />
183
+ ) : (
184
+ <p className="p-4 text-sm text-muted-foreground">
185
+ {gitState.snapshot.status === 'loading' || gitState.snapshot.status === 'idle'
186
+ ? t('findingGit')
187
+ : (gitState.snapshot.error ?? t('noGit'))}
188
+ </p>
189
+ )}
190
+ </div>
191
+ )
192
+ const upper = (
193
+ <ResizablePanelGroup
194
+ key={directoryOpen ? 'tree-visible' : 'tree-hidden'}
195
+ orientation="horizontal"
196
+ defaultLayout={
197
+ directoryOpen
198
+ ? { 'project-tree': global.directorySize, 'project-code': 100 - global.directorySize }
199
+ : undefined
200
+ }
201
+ onLayoutChanged={(layout, meta) => {
202
+ if (meta.isUserInteraction && layout['project-tree'] !== undefined)
203
+ updateGlobal({ directorySize: layout['project-tree'] })
204
+ }}
205
+ >
206
+ {directoryOpen ? (
207
+ <ResizablePanel
208
+ id="project-tree"
209
+ defaultSize={`${global.directorySize}%`}
210
+ minSize="180px"
211
+ maxSize="40%"
212
+ >
213
+ {fileTree}
214
+ </ResizablePanel>
215
+ ) : null}
216
+ {directoryOpen ? <ResizableHandle /> : null}
217
+ <ResizablePanel id="project-code" minSize="40%">
218
+ {code}
219
+ </ResizablePanel>
220
+ </ResizablePanelGroup>
221
+ )
222
+
223
+ return (
224
+ <div
225
+ ref={setContainer}
226
+ className="flex size-full min-h-0 min-w-0 flex-col overflow-hidden"
227
+ data-testid="project-preview"
228
+ >
229
+ <header className="flex min-h-14 shrink-0 flex-wrap items-center gap-2 border-b bg-muted px-3 py-2 pt-[max(8px,env(safe-area-inset-top))]">
230
+ {!stacked ? (
231
+ <Button
232
+ size="icon-sm"
233
+ variant="ghost"
234
+ aria-label={t(directoryOpen ? 'collapseDirectory' : 'expandDirectory')}
235
+ onClick={() => updateGlobal({ directoryOpen: !directoryOpen })}
236
+ >
237
+ {directoryOpen ? (
238
+ <PanelLeftCloseIcon className="size-4" />
239
+ ) : (
240
+ <PanelLeftOpenIcon className="size-4" />
241
+ )}
242
+ </Button>
243
+ ) : null}
244
+ <div className="min-w-0 flex-1">
245
+ {title ?? (
246
+ <h1 className="truncate text-lg font-semibold" title={cwd}>
247
+ {projectName}
248
+ </h1>
249
+ )}
250
+ <p className="truncate text-xs text-muted-foreground" title={cwd}>
251
+ {cwd}
252
+ </p>
253
+ </div>
254
+ <Button
255
+ size="sm"
256
+ variant={gitOpen ? 'secondary' : 'ghost'}
257
+ aria-pressed={gitOpen}
258
+ onClick={() => openGit()}
259
+ >
260
+ <GitBranchIcon className="size-4" />
261
+ <span className="max-sm:hidden">{t('gitRecords')}</span>
262
+ </Button>
263
+ <Button size="icon-sm" variant="ghost" aria-label={t('refreshPreview')} onClick={refresh}>
264
+ <RefreshCwIcon className="size-4" />
265
+ </Button>
266
+ {actions}
267
+ </header>
268
+ {gitState.snapshot.error ? (
269
+ <p role="alert" className="shrink-0 border-b px-3 py-2 text-sm text-destructive">
270
+ {gitState.snapshot.error}
271
+ </p>
272
+ ) : null}
273
+ {stacked ? (
274
+ <>
275
+ <div
276
+ className="flex shrink-0 gap-1 border-b px-2 py-1"
277
+ role="group"
278
+ aria-label={t('previewArea')}
279
+ >
280
+ <Button
281
+ size="sm"
282
+ variant={page === 'files' ? 'secondary' : 'ghost'}
283
+ onClick={() => setPage('files')}
284
+ >
285
+ <FolderTreeIcon className="size-4" />
286
+ {t('files')}
287
+ </Button>
288
+ <Button
289
+ size="sm"
290
+ variant={page === 'code' ? 'secondary' : 'ghost'}
291
+ onClick={() => setPage('code')}
292
+ >
293
+ <FileCode2Icon className="size-4" />
294
+ {t('code')}
295
+ </Button>
296
+ <Button
297
+ size="sm"
298
+ variant={page === 'git' ? 'secondary' : 'ghost'}
299
+ onClick={() => openGit()}
300
+ >
301
+ <Columns2Icon className="size-4" />
302
+ Git
303
+ </Button>
304
+ </div>
305
+ <div className="min-h-0 flex-1">
306
+ {page === 'files' ? fileTree : page === 'git' ? gitView : code}
307
+ </div>
308
+ </>
309
+ ) : gitMaximized && gitOpen ? (
310
+ gitView
311
+ ) : (
312
+ <ResizablePanelGroup
313
+ key={gitOpen ? 'git-visible' : 'git-hidden'}
314
+ orientation="vertical"
315
+ defaultLayout={
316
+ gitOpen
317
+ ? { 'project-main': 100 - global.gitSize, 'project-git': global.gitSize }
318
+ : undefined
319
+ }
320
+ onLayoutChanged={(layout, meta) => {
321
+ if (meta.isUserInteraction && layout['project-git'] !== undefined)
322
+ updateGlobal({ gitSize: layout['project-git'] })
323
+ }}
324
+ >
325
+ <ResizablePanel
326
+ id="project-main"
327
+ defaultSize={gitOpen ? `${100 - global.gitSize}%` : '100%'}
328
+ minSize="35%"
329
+ >
330
+ {upper}
331
+ </ResizablePanel>
332
+ {gitOpen ? <ResizableHandle /> : null}
333
+ {gitOpen ? (
334
+ <ResizablePanel id="project-git" defaultSize={`${global.gitSize}%`} minSize="300px">
335
+ {gitView}
336
+ </ResizablePanel>
337
+ ) : null}
338
+ </ResizablePanelGroup>
339
+ )}
340
+ </div>
341
+ )
342
+ }
@@ -99,7 +99,7 @@ export class L2ProjectHistoryRuntime {
99
99
  getChangeRows(): readonly L3PathTreeRow[] {
100
100
  return this.changeRows
101
101
  }
102
- restoreBrowsing(input: L2ProjectPreviewProject['history']): void {
102
+ restoreBrowsing(input: L2ProjectPreviewProject['history'], load = true): void {
103
103
  Object.assign(this.state, {
104
104
  repositoryRoot: input.repositoryRoot,
105
105
  mode: input.mode,
@@ -114,7 +114,7 @@ export class L2ProjectHistoryRuntime {
114
114
  this.restoredChangesScroll = input.changesScroll
115
115
  this.restoredCommit = input.selectedCommit
116
116
  this.restoredChangePath = input.selectedChangePath
117
- if (input.repositoryRoot !== null) {
117
+ if (load && input.repositoryRoot !== null) {
118
118
  void this.loadBranches()
119
119
  void this.reload()
120
120
  }
@@ -138,7 +138,13 @@ export class L2ProjectHistoryRuntime {
138
138
  }
139
139
 
140
140
  setRepository(root: string): void {
141
- if (root === this.state.repositoryRoot) return
141
+ if (root === this.state.repositoryRoot) {
142
+ if (this.state.branches.status === 'idle') {
143
+ void this.loadBranches()
144
+ void this.reload()
145
+ }
146
+ return
147
+ }
142
148
  this.cancel('branches')
143
149
  this.state.repositoryRoot = root
144
150
  this.state.tip = this.state.target = 'HEAD'
@@ -0,0 +1,168 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+ import {
6
+ ArrowLeftIcon,
7
+ ChevronRightIcon,
8
+ FolderIcon,
9
+ FolderOpenIcon,
10
+ LoaderCircleIcon
11
+ } from 'lucide-react'
12
+ import { Button } from '@client/l4_foundation/ui/shadcn/button'
13
+ import { Input } from '@client/l4_foundation/ui/shadcn/input'
14
+ import type { L2ProjectPicker } from '../hooks/l2-use-project-picker'
15
+
16
+ export function L2ProjectPickerView({
17
+ picker,
18
+ onSelect
19
+ }: {
20
+ picker: L2ProjectPicker
21
+ onSelect: (cwd: string) => void
22
+ }): React.JSX.Element {
23
+ const { t } = useTranslation('projectHistory')
24
+ const [path, setPath] = useState('')
25
+ const browsing = picker.target.view === 'browser'
26
+ const open = (): void => {
27
+ const directory = path
28
+ .trim()
29
+ .replace(/^(["'])(.*)\1$/, '$2')
30
+ .trim()
31
+ if (directory) onSelect(directory)
32
+ }
33
+ return (
34
+ <section
35
+ className="mx-auto flex w-full max-w-3xl flex-col gap-6 px-4 py-8 sm:px-6 sm:py-12"
36
+ data-testid="project-picker"
37
+ >
38
+ <div>
39
+ <h1 className="text-xl font-semibold">{t('chooseProject')}</h1>
40
+ <p className="mt-2 text-sm text-muted-foreground">{t('serverDirectoryHint')}</p>
41
+ </div>
42
+ <form
43
+ onSubmit={(event) => {
44
+ event.preventDefault()
45
+ open()
46
+ }}
47
+ className="space-y-2"
48
+ >
49
+ <label htmlFor="project-directory" className="text-sm font-medium">
50
+ {t('directoryPath')}
51
+ </label>
52
+ <div className="flex gap-2">
53
+ <Input
54
+ id="project-directory"
55
+ value={path}
56
+ onChange={(event) => setPath(event.target.value)}
57
+ placeholder={t('directoryPlaceholder')}
58
+ autoComplete="off"
59
+ className="min-w-0 flex-1 font-mono"
60
+ onKeyDown={(event) => {
61
+ if (event.key === 'Enter' && event.nativeEvent.isComposing) event.preventDefault()
62
+ }}
63
+ />
64
+ <Button type="submit" disabled={!path.trim()}>
65
+ {t('openProject')}
66
+ </Button>
67
+ </div>
68
+ </form>
69
+ <div className="min-w-0">
70
+ <div className="mb-3 flex flex-wrap items-center gap-2">
71
+ <Button
72
+ variant={browsing ? 'ghost' : 'secondary'}
73
+ onClick={picker.showProjects}
74
+ disabled={picker.loading}
75
+ >
76
+ {t('recentProjects')}
77
+ </Button>
78
+ <Button
79
+ variant={browsing ? 'secondary' : 'ghost'}
80
+ onClick={() => picker.browse(null)}
81
+ disabled={picker.loading}
82
+ >
83
+ <FolderOpenIcon className="size-4" />
84
+ {t('browseDirectories')}
85
+ </Button>
86
+ </div>
87
+ {picker.error ? (
88
+ <div role="alert" className="space-y-3 rounded-lg border p-4">
89
+ <p className="break-words text-sm text-destructive">{picker.error}</p>
90
+ <Button variant="outline" onClick={picker.retry}>
91
+ {t('retry')}
92
+ </Button>
93
+ </div>
94
+ ) : picker.loading ? (
95
+ <div role="status" className="flex items-center gap-2 py-6 text-sm text-muted-foreground">
96
+ <LoaderCircleIcon className="size-4 animate-spin" />
97
+ {t('loadingDirectories')}
98
+ </div>
99
+ ) : browsing && picker.browser ? (
100
+ <div>
101
+ <div className="mb-2 flex items-center gap-2">
102
+ <Button
103
+ size="icon-sm"
104
+ variant="ghost"
105
+ aria-label={t('parentDirectory')}
106
+ disabled={picker.browser.cwd === null}
107
+ onClick={() => picker.browse(picker.browser?.parentCwd ?? null)}
108
+ >
109
+ <ArrowLeftIcon className="size-4" />
110
+ </Button>
111
+ <span className="min-w-0 flex-1 break-all font-mono text-sm">
112
+ {picker.browser.cwd ?? t('directoryRoots')}
113
+ </span>
114
+ {picker.browser.cwd ? (
115
+ <Button size="sm" onClick={() => onSelect(picker.browser!.cwd!)}>
116
+ {t('openThisDirectory')}
117
+ </Button>
118
+ ) : null}
119
+ </div>
120
+ <div className="divide-y rounded-lg border">
121
+ {picker.browser.directories.map((directory) => (
122
+ <button
123
+ key={directory.cwd}
124
+ type="button"
125
+ className="flex min-h-10 w-full items-center gap-2 px-3 py-2 text-left hover:bg-accent focus-visible:outline-2 focus-visible:outline-ring"
126
+ onClick={() => picker.browse(directory.cwd)}
127
+ >
128
+ <FolderIcon className="size-4 shrink-0 text-muted-foreground" />
129
+ <span className="min-w-0 flex-1 break-all text-sm">{directory.name}</span>
130
+ <ChevronRightIcon className="size-4 shrink-0" />
131
+ </button>
132
+ ))}
133
+ {!picker.browser.directories.length ? (
134
+ <p className="p-4 text-sm text-muted-foreground">{t('noSubdirectories')}</p>
135
+ ) : null}
136
+ </div>
137
+ </div>
138
+ ) : (
139
+ <div className="divide-y rounded-lg border">
140
+ {picker.projects.map((project) => (
141
+ <button
142
+ key={project.cwd}
143
+ type="button"
144
+ className="flex min-h-14 w-full items-center gap-3 px-3 py-2 text-left hover:bg-accent focus-visible:outline-2 focus-visible:outline-ring"
145
+ onClick={() => onSelect(project.cwd)}
146
+ >
147
+ <FolderIcon className="size-4 shrink-0 text-muted-foreground" />
148
+ <span className="min-w-0 flex-1">
149
+ <span className="block truncate text-sm font-medium">
150
+ {project.cwd.replaceAll('\\', '/').split('/').filter(Boolean).at(-1) ??
151
+ project.cwd}
152
+ </span>
153
+ <span className="block break-all font-mono text-xs text-muted-foreground">
154
+ {project.cwd}
155
+ </span>
156
+ </span>
157
+ <ChevronRightIcon className="size-4 shrink-0" />
158
+ </button>
159
+ ))}
160
+ {!picker.projects.length ? (
161
+ <p className="p-4 text-sm text-muted-foreground">{t('noRecentProjects')}</p>
162
+ ) : null}
163
+ </div>
164
+ )}
165
+ </div>
166
+ </section>
167
+ )
168
+ }
@@ -196,6 +196,36 @@ export function useL2ModelSettings(
196
196
  }
197
197
  }
198
198
 
199
+ async function confirmModelChanges(
200
+ input: L2ModelSettingsReplaceRequest,
201
+ removedPresetCount = 0
202
+ ): Promise<boolean> {
203
+ const { projectDefaults } = await biz.previewSettings(input)
204
+ if (projectDefaults.length > 0) {
205
+ const description = [
206
+ t('replaceProjectDefaultsDescription'),
207
+ ...(removedPresetCount > 0
208
+ ? [t('removeInvalidPresets', { count: removedPresetCount })]
209
+ : [])
210
+ ].join(' ')
211
+ return confirm({
212
+ title: t('replaceProjectDefaultsTitle'),
213
+ description,
214
+ content: createElement(L2ModelReplacementPreview, { projectDefaults }),
215
+ confirmLabel: t('confirmAndSaveModels'),
216
+ confirmVariant: 'default'
217
+ })
218
+ }
219
+ return (
220
+ removedPresetCount === 0 ||
221
+ confirm({
222
+ title: t('removeInvalidPresetsTitle'),
223
+ description: t('removeInvalidPresets', { count: removedPresetCount }),
224
+ confirmLabel: t('removeAndSave')
225
+ })
226
+ )
227
+ }
228
+
199
229
  async function saveConfig(): Promise<void> {
200
230
  if (savingRef.current || inputState.invalid || serviceDirty || nativeOpen) return
201
231
  if (!settings || !formDirty) {
@@ -230,36 +260,7 @@ export function useL2ModelSettings(
230
260
  ? { modelReplacements: modelReplacements.current }
231
261
  : {})
232
262
  }
233
- const { projectDefaults } = modelReplacements.current.length
234
- ? await biz.previewSettings(input)
235
- : { projectDefaults: [] }
236
- if (projectDefaults.length > 0) {
237
- const description = [
238
- t('replaceProjectDefaultsDescription'),
239
- ...(removedPresetCount > 0
240
- ? [t('removeInvalidPresets', { count: removedPresetCount })]
241
- : [])
242
- ].join(' ')
243
- if (
244
- !(await confirm({
245
- title: t('replaceProjectDefaultsTitle'),
246
- description,
247
- content: createElement(L2ModelReplacementPreview, { projectDefaults }),
248
- confirmLabel: t('confirmAndSaveModels'),
249
- confirmVariant: 'default'
250
- }))
251
- )
252
- return
253
- } else if (
254
- removedPresetCount > 0 &&
255
- !(await confirm({
256
- title: t('removeInvalidPresetsTitle'),
257
- description: t('removeInvalidPresets', { count: removedPresetCount }),
258
- confirmLabel: t('removeAndSave')
259
- }))
260
- ) {
261
- return
262
- }
263
+ if (!(await confirmModelChanges(input, removedPresetCount))) return
263
264
 
264
265
  await biz.replaceSettings(input)
265
266
  applySettings(await biz.getSettings())
@@ -335,7 +336,9 @@ export function useL2ModelSettings(
335
336
  setNativeError(null)
336
337
  try {
337
338
  const nativeConfig = biz.parseNativeConfig(nativeText)
338
- await biz.replaceSettings({ nativeConfig })
339
+ const input = { nativeConfig }
340
+ if (!(await confirmModelChanges(input))) return
341
+ await biz.replaceSettings(input)
339
342
  applySettings(await biz.getSettings())
340
343
  setNativeOpen(false)
341
344
  toast.success(t('modelSaved'))
@@ -24,7 +24,13 @@ export function L2ModelReplacementPreview({
24
24
  {from.provider}/{from.modelId} · {t(THINKING_LABELS[from.thinkingLevel])}
25
25
  <span className="mx-1.5">→</span>
26
26
  <span className="text-foreground">
27
- {to.provider}/{to.modelId} · {t(THINKING_LABELS[to.thinkingLevel])}
27
+ {to ? (
28
+ <>
29
+ {to.provider}/{to.modelId} · {t(THINKING_LABELS[to.thinkingLevel])}
30
+ </>
31
+ ) : (
32
+ t('clearProjectDefault')
33
+ )}
28
34
  </span>
29
35
  </p>
30
36
  </li>
@@ -35,11 +35,12 @@ export function subscribeL2ProjectPreviewDirectories(
35
35
  )
36
36
  syncL2ProjectPreviewGitReferences(files, git, getSessions(), getFocusedWorkId())
37
37
  })
38
- for (const session of getSessions()) {
39
- const state = chat.getSourceState(session)
38
+ for (const { workId, sessionId, branchId } of getSessions()) {
39
+ const source = { workId, sessionId, branchId }
40
+ const state = chat.getSourceState(source)
40
41
  if (state.syncStatus === 'loading') continue
41
42
  files.applyPreviewDirectories(
42
- session,
43
+ source,
43
44
  readL4ProjectPreviewState(state.runtime.plugins[L4_PROJECT_PREVIEW_PLUGIN_KEY])
44
45
  )
45
46
  }