@rexvane/piweb 0.3.18 → 0.3.19

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 (261) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +8 -6
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +4 -4
  5. package/.next/react-loadable-manifest.json +23 -9
  6. package/.next/required-server-files.js +2 -1
  7. package/.next/required-server-files.json +2 -1
  8. package/.next/routes-manifest.json +12 -0
  9. package/.next/server/app/_global-error/page.js +2 -2
  10. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_global-error.html +1 -1
  12. package/.next/server/app/_global-error.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page.js +1 -1
  17. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/_not-found.html +1 -1
  19. package/.next/server/app/_not-found.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/server/app/api/agent/[id]/events/route.js +3 -3
  24. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  25. package/.next/server/app/api/agent/[id]/route.js +1 -1
  26. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  27. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  28. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  29. package/.next/server/app/api/agent/new/route.js +1 -1
  30. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  31. package/.next/server/app/api/config/route.js +1 -1
  32. package/.next/server/app/api/config/route.js.nft.json +1 -1
  33. package/.next/server/app/api/files/route.js +1 -1
  34. package/.next/server/app/api/files/route.js.nft.json +1 -1
  35. package/.next/server/app/api/fs/route.js +1 -0
  36. package/.next/server/app/api/fs/route.js.nft.json +1 -0
  37. package/.next/server/app/api/fs/route_client-reference-manifest.js +1 -0
  38. package/.next/server/app/api/git/route.js +2 -2
  39. package/.next/server/app/api/git/route.js.nft.json +1 -1
  40. package/.next/server/app/api/growth/route.js +1 -1
  41. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  42. package/.next/server/app/api/health/route.js +1 -1
  43. package/.next/server/app/api/models/route.js +1 -1
  44. package/.next/server/app/api/models/route.js.nft.json +1 -1
  45. package/.next/server/app/api/pi-settings/route.js +1 -1
  46. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  47. package/.next/server/app/api/plugins/route.js +1 -1
  48. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  49. package/.next/server/app/api/prompts/route.js +1 -1
  50. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  51. package/.next/server/app/api/remote/route.js +8 -0
  52. package/.next/server/app/api/remote/route.js.nft.json +1 -0
  53. package/.next/server/app/api/remote/route_client-reference-manifest.js +1 -0
  54. package/.next/server/app/api/security/route.js +1 -1
  55. package/.next/server/app/api/security/route.js.nft.json +1 -1
  56. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  57. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  58. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  59. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  60. package/.next/server/app/api/sessions/import/route.js +2 -2
  61. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  62. package/.next/server/app/api/sessions/route.js +1 -1
  63. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  64. package/.next/server/app/api/skills/route.js +1 -1
  65. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  66. package/.next/server/app/api/update/route.js +2 -2
  67. package/.next/server/app/api/update/route.js.nft.json +1 -1
  68. package/.next/server/app/api/version/route.js +1 -1
  69. package/.next/server/app/api/version/route.js.nft.json +1 -1
  70. package/.next/server/app/api/web-auth/route.js +3 -1
  71. package/.next/server/app/api/workspaces/route.js +1 -1
  72. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  73. package/.next/server/app/icon.svg/route.js +1 -1
  74. package/.next/server/app/index.html +1 -1
  75. package/.next/server/app/index.rsc +4 -4
  76. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  77. package/.next/server/app/index.segments/_full.segment.rsc +4 -4
  78. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  79. package/.next/server/app/login/page.js +1 -1
  80. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  81. package/.next/server/app/login.html +1 -1
  82. package/.next/server/app/login.rsc +3 -3
  83. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  84. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  85. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -3
  86. package/.next/server/app/page.js +1 -1
  87. package/.next/server/app/page.js.nft.json +1 -1
  88. package/.next/server/app/page_client-reference-manifest.js +1 -1
  89. package/.next/server/app-paths-manifest.json +8 -6
  90. package/.next/server/chunks/229.js +1 -1
  91. package/.next/server/chunks/243.js +1 -0
  92. package/.next/server/chunks/393.js +1 -0
  93. package/.next/server/chunks/496.js +1 -1
  94. package/.next/server/chunks/538.js +1 -0
  95. package/.next/server/chunks/686.js +1 -4
  96. package/.next/server/chunks/802.js +2 -0
  97. package/.next/server/chunks/807.js +1 -0
  98. package/.next/server/chunks/920.js +4 -0
  99. package/.next/server/chunks/924.js +6 -0
  100. package/.next/server/instrumentation.js +1 -0
  101. package/.next/server/instrumentation.js.nft.json +1 -0
  102. package/.next/server/middleware-build-manifest.js +1 -1
  103. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  104. package/.next/server/middleware.js +4 -3
  105. package/.next/server/pages/404.html +1 -1
  106. package/.next/server/pages/500.html +1 -1
  107. package/.next/server/server-reference-manifest.json +1 -1
  108. package/.next/static/JDGSR-TIbk9qZWcUNYIX3/_buildManifest.js +1 -0
  109. package/.next/static/chunks/2716.7ad073038e94a802.js +1 -0
  110. package/.next/static/chunks/316-5ea443ee6b512aa9.js +1 -0
  111. package/.next/static/chunks/4673.8a5a9e6855350e34.js +1 -0
  112. package/.next/static/chunks/4686-e3825f3b6a384aa1.js +1 -0
  113. package/.next/static/chunks/4778.e4c9ed093a413bc4.js +1 -0
  114. package/.next/static/chunks/5173.91ddb803785009f8.js +1 -0
  115. package/.next/static/chunks/839.7403c6bdace4c56c.js +1 -0
  116. package/.next/static/chunks/8552.6bfe3e101250b20f.js +1 -0
  117. package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +1 -0
  118. package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +1 -0
  119. package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +1 -0
  120. package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +1 -0
  121. package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +1 -0
  122. package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +1 -0
  123. package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +1 -0
  124. package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +1 -0
  125. package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +1 -0
  126. package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +1 -0
  127. package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +1 -0
  128. package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +1 -0
  129. package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +1 -0
  130. package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +1 -0
  131. package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +1 -0
  132. package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +1 -0
  133. package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +1 -0
  134. package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +1 -0
  135. package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +1 -0
  136. package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +1 -0
  137. package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +1 -0
  138. package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +1 -0
  139. package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +1 -0
  140. package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +1 -0
  141. package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +1 -0
  142. package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +1 -0
  143. package/.next/static/chunks/app/login/page-5e276a1eeb8867df.js +1 -0
  144. package/.next/static/chunks/app/page-2e425ef83fba0782.js +1 -0
  145. package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +1 -0
  146. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +1 -0
  147. package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +1 -0
  148. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +1 -0
  149. package/.next/static/chunks/webpack-bd2b9202ecbcdcaa.js +1 -0
  150. package/.next/static/css/7391c42e1ae617f9.css +1 -0
  151. package/.next/types/app/api/fs/route.ts +353 -0
  152. package/.next/types/app/api/remote/route.ts +353 -0
  153. package/.next/types/routes.d.ts +3 -1
  154. package/.next/types/validator.ts +18 -0
  155. package/README.md +56 -10
  156. package/next.config.ts +3 -2
  157. package/package.json +3 -3
  158. package/scripts/launcher.mjs +194 -13
  159. package/scripts/release.mjs +0 -2
  160. package/src/app/api/agent/[id]/events/route.ts +11 -6
  161. package/src/app/api/fs/route.ts +119 -0
  162. package/src/app/api/growth/route.ts +11 -6
  163. package/src/app/api/models/route.ts +21 -9
  164. package/src/app/api/remote/route.ts +67 -0
  165. package/src/app/api/sessions/[id]/export/route.ts +11 -1
  166. package/src/app/api/sessions/route.ts +3 -1
  167. package/src/app/api/web-auth/route.ts +45 -49
  168. package/src/app/api/workspaces/route.ts +16 -0
  169. package/src/app/globals.css +6 -0
  170. package/src/app/login/page.tsx +8 -60
  171. package/src/components/AppShell.tsx +202 -155
  172. package/src/components/ChatInput.tsx +61 -6
  173. package/src/components/ChatWindow.tsx +274 -132
  174. package/src/components/DirectoryPicker.tsx +189 -0
  175. package/src/components/DrawerPanel.tsx +164 -0
  176. package/src/components/FileViewer.tsx +2 -2
  177. package/src/components/GrowthTree.tsx +6 -5
  178. package/src/components/MarkdownBlock.tsx +134 -0
  179. package/src/components/OutlineRail.tsx +22 -1
  180. package/src/components/ProjectPanel.tsx +197 -14
  181. package/src/components/RemoteSection.tsx +232 -0
  182. package/src/components/SessionImportSection.tsx +60 -38
  183. package/src/components/SettingsPanel.tsx +104 -57
  184. package/src/components/icons.tsx +23 -0
  185. package/src/hooks/useGrowth.ts +47 -25
  186. package/src/hooks/usePiWeb.ts +100 -14
  187. package/src/i18n.tsx +115 -28
  188. package/src/instrumentation-node.ts +35 -0
  189. package/src/instrumentation.ts +18 -0
  190. package/src/lib/agent-config.ts +134 -0
  191. package/src/lib/agent-manager.ts +243 -61
  192. package/src/lib/auth-throttle.ts +102 -0
  193. package/src/lib/auth.ts +35 -27
  194. package/src/lib/files-service.ts +61 -1
  195. package/src/lib/font.ts +84 -0
  196. package/src/lib/git-service.ts +5 -3
  197. package/src/lib/growth-service.ts +78 -30
  198. package/src/lib/growth-tracker.ts +91 -9
  199. package/src/lib/models-service.ts +36 -11
  200. package/src/lib/pi-settings.ts +10 -12
  201. package/src/lib/pi.ts +145 -18
  202. package/src/lib/plugins-service.ts +67 -35
  203. package/src/lib/remote/assets.json +4 -0
  204. package/src/lib/remote/mobile-proxy.ts +149 -0
  205. package/src/lib/remote/supervisor.ts +482 -0
  206. package/src/lib/security-service.ts +6 -11
  207. package/src/lib/session-import/claude.ts +45 -21
  208. package/src/lib/session-reader.ts +106 -3
  209. package/src/lib/skills-service.ts +34 -12
  210. package/src/lib/text-sanitize.ts +12 -0
  211. package/src/lib/theme.ts +3 -0
  212. package/src/lib/types.ts +16 -2
  213. package/src/lib/update-service.ts +0 -1
  214. package/src/lib/web-auth-shared.ts +5 -1
  215. package/src/lib/web-auth.ts +139 -22
  216. package/src/lib/web-launch-token.ts +55 -0
  217. package/src/lib/web-secret.ts +182 -0
  218. package/src/lib/workspace-store.ts +91 -7
  219. package/src/proxy.ts +46 -12
  220. package/.next/server/chunks/532.js +0 -1
  221. package/.next/static/chunks/2155.2f27fe7098987699.js +0 -1
  222. package/.next/static/chunks/2608.ec8ff1f5767c9056.js +0 -1
  223. package/.next/static/chunks/2716.93be8ad5c970c516.js +0 -1
  224. package/.next/static/chunks/4632.112d2e893d48d188.js +0 -1
  225. package/.next/static/chunks/4673.c1fb1580efc6ace7.js +0 -1
  226. package/.next/static/chunks/4686-f4a037d609cea3f2.js +0 -1
  227. package/.next/static/chunks/692-7992db0ae9cc3021.js +0 -1
  228. package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +0 -1
  229. package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +0 -1
  230. package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +0 -1
  231. package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +0 -1
  232. package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +0 -1
  233. package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +0 -1
  234. package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +0 -1
  235. package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +0 -1
  236. package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +0 -1
  237. package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +0 -1
  238. package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +0 -1
  239. package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +0 -1
  240. package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +0 -1
  241. package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +0 -1
  242. package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +0 -1
  243. package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +0 -1
  244. package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +0 -1
  245. package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +0 -1
  246. package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +0 -1
  247. package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +0 -1
  248. package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +0 -1
  249. package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +0 -1
  250. package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +0 -1
  251. package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +0 -1
  252. package/.next/static/chunks/app/login/page-52ef4841cbd199a8.js +0 -1
  253. package/.next/static/chunks/app/page-b51087831582ee37.js +0 -1
  254. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +0 -1
  255. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +0 -1
  256. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +0 -1
  257. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +0 -1
  258. package/.next/static/chunks/webpack-c31263fd83d75ce6.js +0 -1
  259. package/.next/static/css/0cd968b9b62035b5.css +0 -1
  260. package/.next/static/en03hnPiuro0rnf8QkFEe/_buildManifest.js +0 -1
  261. /package/.next/static/{en03hnPiuro0rnf8QkFEe → JDGSR-TIbk9qZWcUNYIX3}/_ssgManifest.js +0 -0
@@ -11,7 +11,8 @@ import { ChatWindow, SessionStatsBar, type RoundBadge } from "@/components/ChatW
11
11
  import { ExtensionDialogHost, ExtensionNotices } from "@/components/ExtensionUI";
12
12
  import dynamic from "next/dynamic";
13
13
  import { SessionSidebar } from "@/components/SessionSidebar";
14
- import { PromptPanel } from "@/components/PromptPanel";
14
+ import type { DrawerSection } from "@/components/DrawerPanel";
15
+ import { DirectoryPicker } from "@/components/DirectoryPicker";
15
16
 
16
17
  // 首屏不需要的重组件按需加载(设置面板含供应商配置与代码高亮,轨迹/文件只在打开时才用)
17
18
  const SettingsPanel = dynamic(() => import("@/components/SettingsPanel").then((m) => m.SettingsPanel), { ssr: false });
@@ -20,16 +21,16 @@ const TrajInspector = dynamic(() => import("@/components/TrajectoryView").then((
20
21
  /** 会话/工作区路径比较(Windows 大小写与分隔符差异不该影响判断) */
21
22
  const samePath = (a: string, b: string): boolean =>
22
23
  a.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase() === b.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
23
- const ProjectPanel = dynamic(() => import("@/components/ProjectPanel").then((m) => m.ProjectPanel), { ssr: false });
24
+ const DrawerPanel = dynamic(() => import("@/components/DrawerPanel").then((m) => m.DrawerPanel), { ssr: false });
25
+ const GrowthConsentDialog = dynamic(() => import("@/components/ProjectPanel").then((m) => m.GrowthConsentDialog), { ssr: false });
24
26
  const FileViewer = dynamic(() => import("@/components/FileViewer").then((m) => m.FileViewer), { ssr: false });
25
- const GitPanel = dynamic(() => import("@/components/GitPanel").then((m) => m.GitPanel), { ssr: false });
27
+ // Git 面板不再由 AppShell 直接渲染:它现在是抽屉里的一个入口(DrawerPanel 内部按需加载)
26
28
  import {
27
- IconAgentPresetOutline16,
28
29
  IconCheckOutline14,
29
30
  IconChevronDown14,
30
31
  IconFolderClose16,
31
- IconFolderOpenOutline16,
32
32
  IconPanelLeftOutline16,
33
+ IconPanelRightOutline16,
33
34
  IconProjectAddOutline16,
34
35
  } from "@/components/icons";
35
36
  import { useI18n } from "@/i18n";
@@ -46,12 +47,11 @@ const SIDEBAR_MIN = 264;
46
47
  const SIDEBAR_MAX = 420;
47
48
  const SIDEBAR_DEFAULT = 280;
48
49
  const SIDEBAR_COLLAPSED = 56;
49
- const DETAILS_MIN = 300;
50
- const DETAILS_MAX = 760;
51
- const DETAILS_DEFAULT = 360;
52
- const PROJECT_MIN = 280;
53
- const PROJECT_MAX = 640;
54
- const PROJECT_DEFAULT = 380;
50
+ // 最右一列(网格列,不是浮层):轨迹详情 ⇄ 抽屉共用同一列,所以宽度也只有一套。
51
+ // 下界取原抽屉(顶部入口列表 + 内容),上界取原详情栏(diff 与轨迹详情需要更宽)。
52
+ const RIGHT_MIN = 280;
53
+ const RIGHT_MAX = 760;
54
+ const RIGHT_DEFAULT = 360;
55
55
  function persist(key: string, value: number) {
56
56
  localStorage.setItem(key, String(value));
57
57
  }
@@ -59,6 +59,19 @@ function restore(key: string, fallback: number): number {
59
59
  const v = parseInt(localStorage.getItem(key) ?? "", 10);
60
60
  return Number.isNaN(v) ? fallback : v;
61
61
  }
62
+ /**
63
+ * 最右一列的宽度:合并前有两个键(piweb.drawerW 抽屉 / piweb.detailsW 详情栏),
64
+ * 现在同一列只写 piweb.drawerW(抽屉是这一列的常驻内容,详情栏已不再单独存在)。
65
+ * 读取时先抽屉键、再回落到旧的详情栏键,最后夹到统一后的上下界——
66
+ * 老用户拖过的那一列宽度不会被重置成默认值,两个键都写过时以较新的抽屉键为准。
67
+ */
68
+ function restoreRightWidth(): number {
69
+ for (const key of ["piweb.drawerW", "piweb.detailsW"]) {
70
+ const v = parseInt(localStorage.getItem(key) ?? "", 10);
71
+ if (!Number.isNaN(v)) return Math.max(RIGHT_MIN, Math.min(RIGHT_MAX, v));
72
+ }
73
+ return RIGHT_DEFAULT;
74
+ }
62
75
 
63
76
  export function AppShell() {
64
77
  const { t } = useI18n();
@@ -86,6 +99,9 @@ export function AppShell() {
86
99
  setGroupBy,
87
100
  setOrderBy,
88
101
  addWorkspaceByPicker,
102
+ dirPickerOpen,
103
+ confirmDirPicker,
104
+ cancelDirPicker,
89
105
  removeWorkspace,
90
106
  refreshModels,
91
107
  sendCommand,
@@ -97,15 +113,14 @@ export function AppShell() {
97
113
  clearCompaction,
98
114
  setError,
99
115
  answerExtensionDialog,
116
+ answerGrowthConsent,
100
117
  dismissExtensionNotice,
101
118
  } = usePiWeb();
102
119
 
103
120
  const [sidebarWidth, setSidebarWidth] = useState(SIDEBAR_DEFAULT);
104
121
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
105
- const [detailsWidth, setDetailsWidth] = useState(DETAILS_DEFAULT);
106
- /** 项目栏(侧栏与对话之间的第四列:项目生长可视化) */
107
- const [projectOpen, setProjectOpen] = useState(false);
108
- const [projectWidth, setProjectWidth] = useState(PROJECT_DEFAULT);
122
+ /** 最右一列(轨迹详情 / 抽屉共用)的宽度 */
123
+ const [rightWidth, setRightWidth] = useState(RIGHT_DEFAULT);
109
124
  // Drafts are parent-owned data, never a replayable last insertion/upload event.
110
125
  // A hero draft keeps its identity when newSession switches the rendered composer.
111
126
  const composerDrafts = useRef(new Map<string, ChatDraft>());
@@ -135,12 +150,17 @@ export function AppShell() {
135
150
  }, []);
136
151
  const saveSessionDraft = useCallback((update: ChatDraftUpdate) => updateDraft(draftKey, update), [draftKey, updateDraft]);
137
152
  const saveHeroDraft = useCallback((update: ChatDraftUpdate) => updateDraft(heroDraftKey, update), [heroDraftKey, updateDraft]);
138
- const [dragging, setDragging] = useState<"sidebar" | "details" | "project" | null>(null);
139
153
  const [settingsOpen, setSettingsOpen] = useState(false);
140
154
  const [selected, setSelected] = useState<TrajEntry | null>(null);
141
- const [gitDetailsOpen, setGitDetailsOpen] = useState(false);
142
- /** 提示词来源面板(右侧详情列) */
143
- const [promptsOpen, setPromptsOpen] = useState(false);
155
+ /** 抽屉当前入口(由这里持有:轨迹详情关掉后要回到同一个入口) */
156
+ // 默认进「项目」:项目栏降级成抽屉入口后,它是最常用的那一个
157
+ const [drawerSection, setDrawerSection] = useState<DrawerSection>("project");
158
+ /** 右侧抽屉(最右一列):入口列表 + 内容 */
159
+ const [drawerOpen, setDrawerOpen] = useState(false);
160
+ /** 打开抽屉时「落到最新轮」只跳过这一次(点 badge 指定轮次时用) */
161
+ const skipLatestOnce = useRef(false);
162
+ /** useGrowth 的"落到最新轮":它在 AppShell 里、不随抽屉卸载,所以必须显式调用 */
163
+ const growthSelectLatest = useRef<() => void>(() => {});
144
164
  const [heroCwd, setHeroCwd] = useState("");
145
165
  const [heroModel, setHeroModel] = useState<{ provider: string; id: string } | null>(null);
146
166
  const [heroThinking, setHeroThinking] = useState("");
@@ -148,29 +168,40 @@ export function AppShell() {
148
168
  const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
149
169
  useEffect(() => {
150
170
  setSidebarWidth(restore("piweb.sidebarW", SIDEBAR_DEFAULT));
151
- setDetailsWidth(restore("piweb.detailsW", DETAILS_DEFAULT));
152
- setProjectWidth(restore("piweb.projectW", PROJECT_DEFAULT));
171
+ setRightWidth(restoreRightWidth());
153
172
  setSidebarCollapsed(localStorage.getItem("piweb.sidebarCollapsed") === "1");
154
- setProjectOpen(localStorage.getItem("piweb.projectOpen") === "1");
173
+ setDrawerOpen(localStorage.getItem("piweb.drawerOpen") === "1");
155
174
  }, []);
156
- const toggleProject = useCallback((next?: boolean) => {
157
- setProjectOpen((cur) => {
175
+ /** 右侧抽屉开关(项目栏降级成抽屉入口后,它是"最右一列是否打开"唯一的开关) */
176
+ const toggleDrawer = useCallback((next?: boolean) => {
177
+ setDrawerOpen((cur) => {
158
178
  const v = next ?? !cur;
159
- localStorage.setItem("piweb.projectOpen", v ? "1" : "0");
179
+ localStorage.setItem("piweb.drawerOpen", v ? "1" : "0");
160
180
  return v;
161
181
  });
162
182
  }, []);
163
- // Ctrl/⌘+Shift+E:切换项目栏(与 VS Code 资源管理器同键)
183
+ /** 切到某个抽屉入口并打开抽屉(顶栏图标 / 点 badge 都用这条) */
184
+ const openDrawerSection = useCallback(
185
+ (section: DrawerSection) => {
186
+ setDrawerSection(section);
187
+ setSelected(null);
188
+ toggleDrawer(true);
189
+ },
190
+ [toggleDrawer],
191
+ );
192
+ // Ctrl/⌘+Shift+E:打开抽屉并切到「项目」入口;已经停在该入口时再按收起。
193
+ // 轨迹详情占着这一列时,这个快捷键把抽屉叫回来(而不是切到轨迹详情)。
164
194
  useEffect(() => {
165
195
  const h = (e: KeyboardEvent) => {
166
196
  if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === "e") {
167
197
  e.preventDefault();
168
- toggleProject();
198
+ if (drawerOpen && !selected && drawerSection === "project") toggleDrawer(false);
199
+ else openDrawerSection("project");
169
200
  }
170
201
  };
171
202
  document.addEventListener("keydown", h);
172
203
  return () => document.removeEventListener("keydown", h);
173
- }, [toggleProject]);
204
+ }, [drawerOpen, drawerSection, toggleDrawer, openDrawerSection]);
174
205
 
175
206
  useEffect(() => {
176
207
  const query = window.matchMedia("(max-width: 840px)");
@@ -200,11 +231,16 @@ export function AppShell() {
200
231
  };
201
232
  }, []);
202
233
 
203
- // 切换会话时清掉轨迹页的选中态并收起详情栏
234
+ // Markdown 分块(react-markdown + highlight.js 常用语言集,实测 472KB)空闲预取:
235
+ // 首屏不为它付出下载成本,等真正打开会话时通常已经就绪,不会看到正文空窗。
236
+ useEffect(() => {
237
+ const timer = setTimeout(() => { void import("@/components/MarkdownBlock"); }, 1200);
238
+ return () => clearTimeout(timer);
239
+ }, []);
240
+
241
+ // 切换会话时清掉轨迹详情的选中态(最右一列回落到抽屉;抽屉入口保持不变)
204
242
  useEffect(() => {
205
243
  setSelected(null);
206
- setGitDetailsOpen(false);
207
- setPromptsOpen(false);
208
244
  }, [currentPath]);
209
245
 
210
246
  // 已知工作区列表(给 Hero 建议);启动不预选任何工作区,
@@ -251,26 +287,32 @@ export function AppShell() {
251
287
 
252
288
 
253
289
  // ---------- 拖拽 ----------
290
+ // pointermove 事件率(125~1000Hz)远高于帧率:事件里只记最新位置,rAF 里每帧
291
+ // 最多提交一次 state,渲染+重排从事件率降到帧率;持久化推迟到松手一次完成。
254
292
  const onDrag = useCallback(
255
- (side: "sidebar" | "details" | "project") => (e: React.PointerEvent) => {
293
+ (side: "sidebar" | "right") => (e: React.PointerEvent) => {
256
294
  e.preventDefault();
257
- setDragging(side);
258
295
  const startX = e.clientX;
259
- const startW = side === "sidebar" ? sidebarWidth : side === "project" ? projectWidth : detailsWidth;
296
+ const startW = side === "sidebar" ? sidebarWidth : rightWidth;
297
+ const [min, max] = side === "sidebar" ? [SIDEBAR_MIN, SIDEBAR_MAX] : [RIGHT_MIN, RIGHT_MAX];
298
+ // 最右一列:左缘往左拖是变宽(与原来的详情栏、抽屉同向)
299
+ const sign = side === "right" ? -1 : 1;
300
+ const commit = side === "sidebar" ? setSidebarWidth : setRightWidth;
301
+ let frame = 0;
302
+ let latest = startW;
260
303
  const move = (ev: PointerEvent) => {
261
- if (side === "sidebar") {
262
- const w = Math.max(SIDEBAR_MIN, Math.min(SIDEBAR_MAX, startW + (ev.clientX - startX)));
263
- setSidebarWidth(w);
264
- } else if (side === "project") {
265
- const w = Math.max(PROJECT_MIN, Math.min(PROJECT_MAX, startW + (ev.clientX - startX)));
266
- setProjectWidth(w);
267
- } else {
268
- const w = Math.max(DETAILS_MIN, Math.min(DETAILS_MAX, startW - (ev.clientX - startX)));
269
- setDetailsWidth(w);
304
+ latest = Math.max(min, Math.min(max, startW + sign * (ev.clientX - startX)));
305
+ if (!frame) {
306
+ frame = requestAnimationFrame(() => {
307
+ frame = 0;
308
+ commit(latest);
309
+ });
270
310
  }
271
311
  };
272
312
  const up = () => {
273
- setDragging(null);
313
+ if (frame) cancelAnimationFrame(frame);
314
+ commit(latest);
315
+ persist(side === "sidebar" ? "piweb.sidebarW" : "piweb.drawerW", latest);
274
316
  window.removeEventListener("pointermove", move);
275
317
  window.removeEventListener("pointerup", up);
276
318
  window.removeEventListener("pointercancel", up);
@@ -280,15 +322,9 @@ export function AppShell() {
280
322
  // 触摸/笔输入被系统手势打断时只触发 pointercancel,不处理会永久泄漏监听器
281
323
  window.addEventListener("pointercancel", up);
282
324
  },
283
- [sidebarWidth, detailsWidth, projectWidth],
325
+ [sidebarWidth, rightWidth],
284
326
  );
285
327
 
286
- useEffect(() => {
287
- if (dragging === "sidebar") persist("piweb.sidebarW", sidebarWidth);
288
- if (dragging === "details") persist("piweb.detailsW", detailsWidth);
289
- if (dragging === "project") persist("piweb.projectW", projectWidth);
290
- }, [dragging, sidebarWidth, detailsWidth, projectWidth]);
291
-
292
328
  const toggleSidebar = () => {
293
329
  const next = !sidebarCollapsed;
294
330
  setSidebarCollapsed(next);
@@ -304,7 +340,7 @@ export function AppShell() {
304
340
  const openToolTrajectory = useCallback((toolCallId: string) => {
305
341
  const entry = trajectoryRef.current.find((item) => item.toolCallId === toolCallId);
306
342
  if (!entry) return;
307
- setGitDetailsOpen(false);
343
+ // 最右一列与抽屉互斥:选中轨迹详情即接管这一列,关掉后回落回抽屉的当前入口
308
344
  setSelected(entry);
309
345
  }, []);
310
346
 
@@ -541,16 +577,20 @@ export function AppShell() {
541
577
  prevStreamingRef.current = isStreaming;
542
578
  }, [isStreaming]);
543
579
 
580
+ /**
581
+ * 「引用到输入框」:引用存成草稿里的独立数组,输入框渲染成 @ 前缀的 chip
582
+ * (不再直接把路径混进正文——正文和引用在视觉上要能分开,发送时才拼成 @路径)。
583
+ */
544
584
  const insertIntoComposer = useCallback((text: string) => {
545
- const textarea = document.querySelector<HTMLTextAreaElement>('[data-testid="composer"] textarea');
546
- const cursor = textarea?.selectionStart;
585
+ // 存的是"裸路径"(不带 @):发送时统一拼 @路径,避免调用方已经带了 @ 就变成 @@
586
+ const path = text.trim().replace(/^@+/, "");
587
+ if (!path) return;
547
588
  updateDraft(draftKey, (previous) => {
548
- const position = Math.min(cursor ?? previous.text.length, previous.text.length);
549
- const before = previous.text.slice(0, position);
550
- const after = previous.text.slice(position);
551
- return { ...previous, text: `${before}${before && !/\s$/.test(before) ? " " : ""}${text}${after}` };
589
+ const references = previous.references ?? [];
590
+ if (references.includes(path)) return previous;
591
+ return { ...previous, references: [...references, path] };
552
592
  });
553
- requestAnimationFrame(() => textarea?.focus());
593
+ requestAnimationFrame(() => document.querySelector<HTMLTextAreaElement>('[data-testid="composer"] textarea')?.focus());
554
594
  }, [draftKey, updateDraft]);
555
595
  const openInEditor = useCallback(
556
596
  async (filePath: string) => {
@@ -579,11 +619,27 @@ export function AppShell() {
579
619
  sessionPath: currentPath,
580
620
  liveRounds: state.growth.rounds,
581
621
  runtimeError: state.growth.error,
582
- active: projectOpen || viewer.state.open,
622
+ active: drawerOpen || viewer.state.open,
583
623
  watchRounds: Boolean(currentPath),
584
624
  connected: state.connected,
585
625
  });
586
626
  const openFileNode = useCallback((node: TreeNode) => viewer.open(node.path, { from: node.from, lazy: node.lazy }), [viewer]);
627
+ // 打开面板时落在最新轮:useGrowth 活在 AppShell 里、不随抽屉卸载,state 不会自己回到最新,
628
+ // 所以每次从「关」变「开」(或换了工作区)都显式选一次最新轮。
629
+ // 新轮次出现时不再自动跳——用户打开面板后停在哪一轮就是哪一轮。
630
+ growthSelectLatest.current = growth.selectLatest;
631
+ useEffect(() => {
632
+ if (!drawerOpen) {
633
+ // 关掉时清掉标记:否则"抽屉已经开着时点 badge"留下的标记会吞掉下一次打开
634
+ skipLatestOnce.current = false;
635
+ return;
636
+ }
637
+ if (skipLatestOnce.current) {
638
+ skipLatestOnce.current = false;
639
+ return;
640
+ }
641
+ growthSelectLatest.current();
642
+ }, [drawerOpen, panelCwd]);
587
643
  const roundBadges = useMemo(() => {
588
644
  const map = new Map<string, RoundBadge>();
589
645
  for (const round of growth.rounds) {
@@ -593,11 +649,16 @@ export function AppShell() {
593
649
  }
594
650
  return map;
595
651
  }, [growth.rounds]);
652
+ // 点对话里的轮次 badge:打开抽屉、切到「项目」入口,并选中**那一轮**(不是最新轮)。
653
+ // skipLatestOnce 让下面「打开即落最新轮」的 effect 放这一拍过去,不要覆盖掉刚选中的那轮。
596
654
  const growthSelect = growth.select;
597
655
  const showRound = useCallback((commit: string) => {
598
- toggleProject(true);
656
+ skipLatestOnce.current = true;
657
+ setDrawerSection("project");
658
+ setSelected(null);
659
+ toggleDrawer(true);
599
660
  growthSelect(commit);
600
- }, [toggleProject, growthSelect]);
661
+ }, [toggleDrawer, growthSelect]);
601
662
  const jumpToChat = useCallback((messageId: string) => {
602
663
  requestAnimationFrame(() => {
603
664
  const node = document.querySelector<HTMLElement>(`[data-role="user"][data-message-id="${CSS.escape(messageId)}"]`);
@@ -612,37 +673,54 @@ export function AppShell() {
612
673
  document.title = currentId && title && title !== "pi" ? `${title} · pi` : "pi";
613
674
  }, [currentId, title]);
614
675
 
615
- // 三栏网格
616
- const gridCols = isNarrow ? "minmax(0,1fr)" : `${sidebarCollapsed ? SIDEBAR_COLLAPSED : sidebarWidth}px ${projectOpen ? `${projectWidth}px` : "0px"} minmax(0,1fr) ${
617
- selected || gitDetailsOpen || promptsOpen ? `${detailsWidth}px` : "0px"
618
- }`;
619
- const projectColumn = projectOpen && (
676
+ // 网格列(宽屏):侧栏 | 对话区 | 最右一列(轨迹详情 ⇄ 抽屉)。项目栏已降级成
677
+ // 抽屉里的一个入口,不再单独占一列,所以是 3 列而不是 4 列。
678
+ // JSX 里各列的渲染顺序必须与这里严格一致,否则网格会把内容摆错列。
679
+ // 最右一列只有一条宽度(rightWidth):详情栏与抽屉合并后不再各占一列。
680
+ const rightColumnVisible = Boolean(selected) || drawerOpen;
681
+ const gridCols = isNarrow ? "minmax(0,1fr)" : `${sidebarCollapsed ? SIDEBAR_COLLAPSED : sidebarWidth}px minmax(0,1fr) ${
682
+ rightColumnVisible ? `${rightWidth}px` : "0px"}`;
683
+
684
+ // 最右一列(宽屏是网格列,窄屏是贴左右的 fixed 覆盖层):轨迹详情 ⇄ 抽屉互斥。
685
+ // selected(点对话里的工具行)优先——它临时接管这一列;关掉后自动回落回抽屉,
686
+ // 并回到抽屉原本选中的入口(入口状态在 drawerSection,不随锁定/解锁重置)。
687
+ const rightColumn = (
620
688
  <div
621
- className="min-h-0 overflow-hidden"
689
+ className="flex min-h-0 flex-col overflow-hidden"
622
690
  style={isNarrow ? {
623
691
  position: "fixed",
624
- inset: "0 auto 0 0",
625
- width: "min(88vw, 420px)",
626
- zIndex: 88,
692
+ inset: "0 0 0 auto",
693
+ width: "min(92vw, 640px)",
694
+ zIndex: 90,
695
+ background: "var(--dsw-sidebar-fill)",
627
696
  boxShadow: "var(--dsw-elevation-prominent)",
628
- } : undefined}
697
+ } : { background: "var(--dsw-sidebar-fill)", borderLeft: "0.5px solid var(--dsw-border-l2)" }}
629
698
  >
630
- <ProjectPanel
631
- growth={growth}
632
- workspaceName={panelCwd ? getWorkspaceName(panelCwd) : ""}
633
- cwd={panelCwd}
634
- hasSession={Boolean(currentPath)}
635
- running={isStreaming}
636
- onJumpToChat={jumpToChat}
637
- onOpenFile={openFileNode}
638
- onReference={insertIntoComposer}
639
- onOpenEditor={openInEditor}
640
- gitRefreshKey={panelRefreshKey}
641
- onAskCommit={() => insertIntoComposer(t.gitAskCommitPrompt)}
642
- onOpenGit={() => { setSelected(null); setGitDetailsOpen(true); }}
643
- onClose={() => toggleProject(false)}
644
- onError={setError}
645
- />
699
+ {selected ? (
700
+ <TrajInspector entry={selected} onClose={() => setSelected(null)} />
701
+ ) : (
702
+ <DrawerPanel
703
+ cwd={panelCwd}
704
+ section={drawerSection}
705
+ onSectionChange={setDrawerSection}
706
+ onClose={() => toggleDrawer(false)}
707
+ sessionId={currentId}
708
+ promptRefreshKey={currentId}
709
+ gitRefreshKey={panelRefreshKey}
710
+ growth={growth}
711
+ workspaceName={panelCwd ? getWorkspaceName(panelCwd) : ""}
712
+ hasSession={Boolean(currentPath)}
713
+ running={isStreaming}
714
+ onOpenFileNode={openFileNode}
715
+ onReference={insertIntoComposer}
716
+ onOpenEditor={openInEditor}
717
+ onJumpToChat={jumpToChat}
718
+ onError={setError}
719
+ onOpenContent={(path, content) => viewer.openStatic(path, content)}
720
+ onAskCommit={() => insertIntoComposer(t.gitAskCommitPrompt)}
721
+ onOpenFile={openInEditor}
722
+ />
723
+ )}
646
724
  </div>
647
725
  );
648
726
 
@@ -651,7 +729,9 @@ export function AppShell() {
651
729
  className="grid h-screen w-screen overflow-hidden"
652
730
  style={{
653
731
  gridTemplateColumns: gridCols,
654
- transition: dragging ? "none" : "grid-template-columns var(--ds-duration-slow) var(--ds-ease-in-out)",
732
+ // 收起/展开不做过渡:grid-template-columns 是布局属性,动画它会让浏览器逐帧
733
+ // 重排整棵布局树,而聊天列 minmax(0,1fr) 的宽度逐帧变化,正文/代码块每帧
734
+ // 重新折行,长对话必然掉帧。瞬时切换只付一次重排;拖动改宽原本也是 none。
655
735
  background: "var(--dsw-bg-base)",
656
736
  }}
657
737
  >
@@ -716,8 +796,7 @@ export function AppShell() {
716
796
  />
717
797
  </div>
718
798
 
719
- {/* 四列网格中始终保留项目列,否则关闭项目栏时会话区会落进 0px 列。 */}
720
- {!isNarrow && (projectColumn || <div aria-hidden="true" />)}
799
+ {/* 三列网格:侧栏 | 会话区 | 最右一列(项目栏已进抽屉,不再有独立的项目列) */}
721
800
 
722
801
  {/* 会话区 */}
723
802
  <div className="pi-main flex min-h-0 min-w-0 flex-col">
@@ -776,33 +855,28 @@ export function AppShell() {
776
855
  <span className="min-w-0 flex-1 truncate" style={{ fontSize: 14.5, fontWeight: 600 }}>
777
856
  {title}
778
857
  </span>
779
- {/* 项目生长入口;轨迹详情由对话内工具行点击唤起 */}
858
+ {/* 项目与提示词原来是两个平级图标,现已合并进右侧抽屉:
859
+ 抽屉顶部那条横向图标栏就是它们的新位置,顶栏只留一个抽屉开关。
860
+ Ctrl/⌘+Shift+E 仍然直达项目入口(见快捷键绑定)。 */}
861
+ {/* 右侧抽屉:项目 / 提示词 / Git 详情 / 会话导入 四个入口都在里面 */}
780
862
  <button
781
863
  type="button"
782
864
  className="icon-btn"
783
- style={{ width: 30, height: 30, background: projectOpen ? "var(--dsw-active)" : undefined }}
784
- title={`${t.projectPanel} (Ctrl/⌘+Shift+E)`}
785
- aria-label={t.projectPanel}
786
- data-testid="project-toggle"
787
- onClick={() => toggleProject()}
788
- >
789
- <IconFolderOpenOutline16 size={15} />
790
- </button>
791
- {/* 提示词来源:系统提示词 / 记忆 / 技能 / 模板 / 工具提示词集中列出,点开即看 */}
792
- <button
793
- type="button"
794
- className="icon-btn"
795
- style={{ width: 30, height: 30, background: promptsOpen ? "var(--dsw-active)" : undefined }}
796
- title={t.promptPanel}
797
- aria-label={t.promptPanel}
798
- data-testid="prompt-panel-toggle"
865
+ style={{ width: 30, height: 30, background: drawerOpen && !selected ? "var(--dsw-active)" : undefined }}
866
+ title={t.drawerPanel}
867
+ aria-label={t.drawerPanel}
868
+ data-testid="drawer-toggle"
799
869
  onClick={() => {
800
- setPromptsOpen((open) => !open);
801
- setSelected(null);
802
- setGitDetailsOpen(false);
870
+ // 轨迹详情占着这一列时,这个按钮是把抽屉叫回来(否则点了像没反应)
871
+ if (selected) {
872
+ setSelected(null);
873
+ toggleDrawer(true);
874
+ return;
875
+ }
876
+ toggleDrawer();
803
877
  }}
804
878
  >
805
- <IconAgentPresetOutline16 size={15} />
879
+ <IconPanelRightOutline16 size={15} />
806
880
  </button>
807
881
  </div>
808
882
  {projectTrust?.required && !projectTrust.trusted && (
@@ -916,36 +990,9 @@ export function AppShell() {
916
990
  )}
917
991
  </div>
918
992
 
919
- {/* 提示词来源 / 轨迹 / Git 详情栏 */}
920
- {(selected || gitDetailsOpen || promptsOpen) && (
921
- <div
922
- className="flex min-h-0 flex-col overflow-hidden"
923
- style={isNarrow ? {
924
- position: "fixed",
925
- inset: "0 0 0 auto",
926
- width: "min(92vw, 640px)",
927
- zIndex: 90,
928
- background: "var(--dsw-sidebar-fill)",
929
- boxShadow: "var(--dsw-elevation-prominent)",
930
- } : { background: "var(--dsw-sidebar-fill)", borderLeft: "0.5px solid var(--dsw-border-l2)" }}
931
- >
932
- <div className="min-h-0 flex-1">
933
- {promptsOpen ? (
934
- <PromptPanel
935
- cwd={panelCwd}
936
- sessionId={currentId}
937
- refreshKey={currentId}
938
- onOpenContent={(path, content) => viewer.openStatic(path, content)}
939
- onClose={() => setPromptsOpen(false)}
940
- />
941
- ) : selected ? (
942
- <TrajInspector entry={selected} onClose={() => setSelected(null)} />
943
- ) : (
944
- <GitPanel cwd={panelCwd} refreshKey={panelRefreshKey} onClose={() => setGitDetailsOpen(false)} onAskCommit={() => insertIntoComposer(t.gitAskCommitPrompt)} onOpenFile={openInEditor} />
945
- )}
946
- </div>
947
- </div>
948
- )}
993
+ {/* 宽屏:最右一列(轨迹详情优先,否则抽屉);关闭时补占位列。
994
+ 网格列数与顺序是固定的三列,少一个 item 会让后面的元素落错列。 */}
995
+ {!isNarrow && (rightColumnVisible ? rightColumn : <div aria-hidden="true" />)}
949
996
 
950
997
  {/* 拖拽手柄 */}
951
998
  {!isNarrow && !sidebarCollapsed && (
@@ -955,24 +1002,18 @@ export function AppShell() {
955
1002
  onPointerDown={onDrag("sidebar")}
956
1003
  />
957
1004
  )}
958
- {!isNarrow && (selected || gitDetailsOpen || promptsOpen) && (
1005
+ {!isNarrow && rightColumnVisible && (
959
1006
  <div
960
1007
  className="fixed top-0 h-full w-2 cursor-col-resize"
961
- style={{ left: `calc(100vw - ${detailsWidth}px - 4px)`, zIndex: 40 }}
962
- onPointerDown={onDrag("details")}
1008
+ style={{ left: `calc(100vw - ${rightWidth}px - 4px)`, zIndex: 40 }}
1009
+ onPointerDown={onDrag("right")}
963
1010
  />
964
1011
  )}
965
- {!isNarrow && projectOpen && (
966
- <div
967
- className="fixed top-0 h-full w-2 cursor-col-resize"
968
- style={{ left: (sidebarCollapsed ? SIDEBAR_COLLAPSED : sidebarWidth) + projectWidth - 4, zIndex: 40 }}
969
- onPointerDown={onDrag("project")}
970
- />
971
- )}
972
- {isNarrow && projectOpen && (
1012
+ {/* 窄屏最右一列:贴右边滑出(轨迹详情与抽屉共用,遮罩关闭回到对话) */}
1013
+ {isNarrow && rightColumnVisible && (
973
1014
  <>
974
- <button className="fixed inset-0 z-[85]" style={{ background: "var(--dsw-mask)" }} onClick={() => toggleProject(false)} aria-label={t.close} />
975
- {projectColumn}
1015
+ <button className="fixed inset-0 z-[85]" style={{ background: "var(--dsw-mask)" }} onClick={() => (selected ? setSelected(null) : toggleDrawer(false))} aria-label={t.close} />
1016
+ {rightColumn}
976
1017
  </>
977
1018
  )}
978
1019
  {viewer.state.open && (
@@ -990,6 +1031,12 @@ export function AppShell() {
990
1031
  )}
991
1032
 
992
1033
  <ExtensionDialogHost dialog={state.extensionDialogs[0] ?? null} onAnswer={(id, response) => void answerExtensionDialog(id, response)} />
1034
+ {/* 扩展对话框优先:它在阻塞 pi,建仓库的询问可以等 */}
1035
+ {state.growth.consent && !state.extensionDialogs.length && (
1036
+ <GrowthConsentDialog request={state.growth.consent} onAnswer={(allow) => void answerGrowthConsent(allow)} />
1037
+ )}
1038
+ {/* 只有远程设备(手机走 tailnet)会走到这里:本机仍然用系统原生对话框 */}
1039
+ <DirectoryPicker open={dirPickerOpen} onConfirm={(path) => void confirmDirPicker(path)} onCancel={cancelDirPicker} />
993
1040
  <ExtensionNotices notices={state.extensionNotices} onDismiss={dismissExtensionNotice} />
994
1041
  <SettingsPanel
995
1042
  open={settingsOpen}