@rexvane/piweb 0.3.15 → 0.3.16

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 (230) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +9 -6
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +6 -6
  5. package/.next/react-loadable-manifest.json +16 -4
  6. package/.next/routes-manifest.json +20 -0
  7. package/.next/server/app/_global-error/page.js +2 -2
  8. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/server/app/_global-error.html +1 -1
  10. package/.next/server/app/_global-error.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/server/app/_not-found/page.js +1 -1
  15. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/server/app/_not-found.html +1 -1
  17. package/.next/server/app/_not-found.rsc +7 -4
  18. package/.next/server/app/_not-found.segments/_full.segment.rsc +7 -4
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -2
  20. package/.next/server/app/_not-found.segments/_tree.segment.rsc +5 -2
  21. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  22. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  23. package/.next/server/app/api/agent/[id]/route.js +1 -1
  24. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  25. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  26. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  27. package/.next/server/app/api/agent/new/route.js +1 -1
  28. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  29. package/.next/server/app/api/config/route.js +1 -1
  30. package/.next/server/app/api/config/route.js.nft.json +1 -1
  31. package/.next/server/app/api/files/route.js +1 -1
  32. package/.next/server/app/api/files/route.js.nft.json +1 -1
  33. package/.next/server/app/api/git/route.js +2 -2
  34. package/.next/server/app/api/git/route.js.nft.json +1 -1
  35. package/.next/server/app/api/growth/route.js +1 -1
  36. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  37. package/.next/server/app/api/health/route.js +1 -1
  38. package/.next/server/app/api/models/route.js +1 -1
  39. package/.next/server/app/api/models/route.js.nft.json +1 -1
  40. package/.next/server/app/api/pi-settings/route.js +1 -1
  41. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  42. package/.next/server/app/api/plugins/route.js +1 -1
  43. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  44. package/.next/server/app/api/prompts/route.js +1 -1
  45. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  46. package/.next/server/app/api/security/route.js +1 -1
  47. package/.next/server/app/api/security/route.js.nft.json +1 -1
  48. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  49. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  50. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  51. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  52. package/.next/server/app/api/sessions/import/route.js +3 -3
  53. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  54. package/.next/server/app/api/sessions/route.js +1 -1
  55. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  56. package/.next/server/app/api/skills/route.js +2 -2
  57. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  58. package/.next/server/app/api/swarm/[id]/route.js +1 -0
  59. package/.next/server/app/api/swarm/[id]/route.js.nft.json +1 -0
  60. package/.next/server/app/api/swarm/[id]/route_client-reference-manifest.js +1 -0
  61. package/.next/server/app/api/swarm/route.js +1 -0
  62. package/.next/server/app/api/swarm/route.js.nft.json +1 -0
  63. package/.next/server/app/api/swarm/route_client-reference-manifest.js +1 -0
  64. package/.next/server/app/api/tests/route.js +2 -0
  65. package/.next/server/app/api/tests/route.js.nft.json +1 -0
  66. package/.next/server/app/api/tests/route_client-reference-manifest.js +1 -0
  67. package/.next/server/app/api/update/route.js +2 -2
  68. package/.next/server/app/api/update/route.js.nft.json +1 -1
  69. package/.next/server/app/api/version/route.js +1 -1
  70. package/.next/server/app/api/version/route.js.nft.json +1 -1
  71. package/.next/server/app/api/web-auth/route.js +1 -1
  72. package/.next/server/app/api/workspaces/route.js +1 -1
  73. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  74. package/.next/server/app/icon.svg/route.js +1 -1
  75. package/.next/server/app/index.html +1 -1
  76. package/.next/server/app/index.rsc +9 -6
  77. package/.next/server/app/index.segments/__PAGE__.segment.rsc +7 -4
  78. package/.next/server/app/index.segments/_full.segment.rsc +9 -6
  79. package/.next/server/app/index.segments/_tree.segment.rsc +5 -2
  80. package/.next/server/app/login/page.js +1 -1
  81. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  82. package/.next/server/app/login.html +1 -1
  83. package/.next/server/app/login.rsc +8 -5
  84. package/.next/server/app/login.segments/_full.segment.rsc +8 -5
  85. package/.next/server/app/login.segments/_tree.segment.rsc +5 -2
  86. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +6 -3
  87. package/.next/server/app/page.js +1 -1
  88. package/.next/server/app/page_client-reference-manifest.js +1 -1
  89. package/.next/server/app-paths-manifest.json +9 -6
  90. package/.next/server/chunks/496.js +1 -1
  91. package/.next/server/chunks/532.js +1 -1
  92. package/.next/server/chunks/686.js +6 -2
  93. package/.next/server/chunks/757.js +1 -0
  94. package/.next/server/chunks/775.js +3 -0
  95. package/.next/server/middleware-build-manifest.js +1 -1
  96. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  97. package/.next/server/pages/404.html +1 -1
  98. package/.next/server/pages/500.html +1 -1
  99. package/.next/server/server-reference-manifest.json +1 -1
  100. package/.next/static/0t1jBUAuVaTFTXxRyIo3q/_buildManifest.js +1 -0
  101. package/.next/static/chunks/2608.43c58236c9cce8e6.js +1 -0
  102. package/.next/static/chunks/{2716.a9e808190a4e2424.js → 2716.4ec42df1d51354c9.js} +1 -1
  103. package/.next/static/chunks/4632.62139b565b657aac.js +1 -0
  104. package/.next/static/chunks/{4673.d3d0a7f2599080fb.js → 4673.7796104395deb8d7.js} +1 -1
  105. package/.next/static/chunks/4686-17f21a006739a596.js +1 -0
  106. package/.next/static/chunks/7577.10af9bb661652da8.js +1 -0
  107. package/.next/static/chunks/8866.38ec4ac4ee5a1b74.js +1 -0
  108. package/.next/static/chunks/app/_global-error/page-5148173b3f55e415.js +1 -0
  109. package/.next/static/chunks/app/api/agent/[id]/events/route-5148173b3f55e415.js +1 -0
  110. package/.next/static/chunks/app/api/agent/[id]/route-5148173b3f55e415.js +1 -0
  111. package/.next/static/chunks/app/api/agent/[id]/tree/route-5148173b3f55e415.js +1 -0
  112. package/.next/static/chunks/app/api/agent/new/route-5148173b3f55e415.js +1 -0
  113. package/.next/static/chunks/app/api/config/route-5148173b3f55e415.js +1 -0
  114. package/.next/static/chunks/app/api/files/route-5148173b3f55e415.js +1 -0
  115. package/.next/static/chunks/app/api/git/route-5148173b3f55e415.js +1 -0
  116. package/.next/static/chunks/app/api/growth/route-5148173b3f55e415.js +1 -0
  117. package/.next/static/chunks/app/api/health/route-5148173b3f55e415.js +1 -0
  118. package/.next/static/chunks/app/api/models/route-5148173b3f55e415.js +1 -0
  119. package/.next/static/chunks/app/api/pi-settings/route-5148173b3f55e415.js +1 -0
  120. package/.next/static/chunks/app/api/plugins/route-5148173b3f55e415.js +1 -0
  121. package/.next/static/chunks/app/api/prompts/route-5148173b3f55e415.js +1 -0
  122. package/.next/static/chunks/app/api/security/route-5148173b3f55e415.js +1 -0
  123. package/.next/static/chunks/app/api/sessions/[id]/export/route-5148173b3f55e415.js +1 -0
  124. package/.next/static/chunks/app/api/sessions/[id]/route-5148173b3f55e415.js +1 -0
  125. package/.next/static/chunks/app/api/sessions/import/route-5148173b3f55e415.js +1 -0
  126. package/.next/static/chunks/app/api/sessions/route-5148173b3f55e415.js +1 -0
  127. package/.next/static/chunks/app/api/skills/route-5148173b3f55e415.js +1 -0
  128. package/.next/static/chunks/app/api/swarm/[id]/route-5148173b3f55e415.js +1 -0
  129. package/.next/static/chunks/app/api/swarm/route-5148173b3f55e415.js +1 -0
  130. package/.next/static/chunks/app/api/tests/route-5148173b3f55e415.js +1 -0
  131. package/.next/static/chunks/app/api/update/route-5148173b3f55e415.js +1 -0
  132. package/.next/static/chunks/app/api/version/route-5148173b3f55e415.js +1 -0
  133. package/.next/static/chunks/app/api/web-auth/route-5148173b3f55e415.js +1 -0
  134. package/.next/static/chunks/app/api/workspaces/route-5148173b3f55e415.js +1 -0
  135. package/.next/static/chunks/app/page-14379b8060d5cf07.js +1 -0
  136. package/.next/static/chunks/next/dist/client/components/builtin/app-error-5148173b3f55e415.js +1 -0
  137. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-5148173b3f55e415.js +1 -0
  138. package/.next/static/chunks/next/dist/client/components/builtin/not-found-5148173b3f55e415.js +1 -0
  139. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-5148173b3f55e415.js +1 -0
  140. package/.next/static/chunks/webpack-d1a69ad9edb38b11.js +1 -0
  141. package/.next/static/css/d0a0162cb481abe8.css +1 -0
  142. package/.next/types/app/api/swarm/[id]/route.ts +353 -0
  143. package/.next/types/app/api/swarm/route.ts +353 -0
  144. package/.next/types/app/api/tests/route.ts +353 -0
  145. package/.next/types/routes.d.ts +4 -1
  146. package/.next/types/validator.ts +27 -0
  147. package/README.md +10 -0
  148. package/package.json +4 -3
  149. package/scripts/pick-folder.ps1 +11 -2
  150. package/scripts/release.mjs +11 -31
  151. package/scripts/run-test-file.mjs +34 -0
  152. package/scripts/verify-release-config.mjs +24 -0
  153. package/src/app/api/config/route.ts +4 -1
  154. package/src/app/api/security/route.ts +8 -3
  155. package/src/app/api/swarm/[id]/route.ts +28 -0
  156. package/src/app/api/swarm/route.ts +24 -0
  157. package/src/app/api/tests/route.ts +27 -0
  158. package/src/app/api/update/route.ts +8 -2
  159. package/src/app/globals.css +777 -0
  160. package/src/app/layout.tsx +13 -6
  161. package/src/components/AppShell.tsx +505 -199
  162. package/src/components/ChatInput.tsx +153 -26
  163. package/src/components/ChatWindow.tsx +46 -40
  164. package/src/components/ContextMeter.tsx +11 -17
  165. package/src/components/FileViewer.tsx +1 -1
  166. package/src/components/GitPanel.tsx +13 -11
  167. package/src/components/ModelSelector.tsx +19 -6
  168. package/src/components/PiMark.tsx +5 -38
  169. package/src/components/ProjectPanel.tsx +1 -0
  170. package/src/components/PromptPanel.tsx +132 -51
  171. package/src/components/SessionImportSection.tsx +17 -17
  172. package/src/components/SessionSidebar.tsx +159 -57
  173. package/src/components/SettingsPanel.tsx +330 -193
  174. package/src/components/SwarmCoordinatorPod.tsx +1021 -0
  175. package/src/components/TestInspectorPod.tsx +549 -0
  176. package/src/components/icons.tsx +23 -0
  177. package/src/hooks/usePiWeb.ts +112 -8
  178. package/src/i18n.tsx +59 -3
  179. package/src/lib/agent-manager.ts +180 -12
  180. package/src/lib/command-validation.ts +9 -1
  181. package/src/lib/delegation.ts +93 -0
  182. package/src/lib/managed-niubash.ts +8 -4
  183. package/src/lib/pi.ts +3 -2
  184. package/src/lib/runtime-activity.ts +27 -0
  185. package/src/lib/session-import/index.ts +5 -0
  186. package/src/lib/session-import/lock.ts +25 -0
  187. package/src/lib/session-import/writer.ts +8 -3
  188. package/src/lib/skills-service.ts +16 -9
  189. package/src/lib/swarm-service.ts +371 -0
  190. package/src/lib/test-run-service.ts +122 -0
  191. package/src/lib/theme.ts +68 -1
  192. package/src/lib/trajectory.ts +1 -0
  193. package/src/lib/types.ts +14 -0
  194. package/src/lib/workflow.ts +55 -0
  195. package/.next/static/DdBwPYoHBKtaJ97QIr6Co/_buildManifest.js +0 -1
  196. package/.next/static/chunks/2608.ec8ff1f5767c9056.js +0 -1
  197. package/.next/static/chunks/4632.0ed92ae42905ce40.js +0 -1
  198. package/.next/static/chunks/4686-7d694def920ff093.js +0 -1
  199. package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +0 -1
  200. package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +0 -1
  201. package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +0 -1
  202. package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +0 -1
  203. package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +0 -1
  204. package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +0 -1
  205. package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +0 -1
  206. package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +0 -1
  207. package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +0 -1
  208. package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +0 -1
  209. package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +0 -1
  210. package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +0 -1
  211. package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +0 -1
  212. package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +0 -1
  213. package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +0 -1
  214. package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +0 -1
  215. package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +0 -1
  216. package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +0 -1
  217. package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +0 -1
  218. package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +0 -1
  219. package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +0 -1
  220. package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +0 -1
  221. package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +0 -1
  222. package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +0 -1
  223. package/.next/static/chunks/app/page-9a5440047c5a415c.js +0 -1
  224. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +0 -1
  225. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +0 -1
  226. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +0 -1
  227. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +0 -1
  228. package/.next/static/chunks/webpack-42c5aae79b0e5d8c.js +0 -1
  229. package/.next/static/css/b2d35abc828d4713.css +0 -1
  230. /package/.next/static/{DdBwPYoHBKtaJ97QIr6Co → 0t1jBUAuVaTFTXxRyIo3q}/_ssgManifest.js +0 -0
@@ -23,14 +23,21 @@ const samePath = (a: string, b: string): boolean =>
23
23
  const ProjectPanel = dynamic(() => import("@/components/ProjectPanel").then((m) => m.ProjectPanel), { ssr: false });
24
24
  const FileViewer = dynamic(() => import("@/components/FileViewer").then((m) => m.FileViewer), { ssr: false });
25
25
  const GitPanel = dynamic(() => import("@/components/GitPanel").then((m) => m.GitPanel), { ssr: false });
26
+ const TestInspectorPod = dynamic(() => import("@/components/TestInspectorPod").then((m) => m.TestInspectorPod), { ssr: false });
27
+ const SwarmCoordinatorPod = dynamic(() => import("@/components/SwarmCoordinatorPod").then((m) => m.SwarmCoordinatorPod), { ssr: false });
26
28
  import {
27
29
  IconAgentPresetOutline16,
30
+ IconArchiveOutline20,
31
+ IconBranchOutline16,
28
32
  IconCheckOutline14,
29
33
  IconChevronDown14,
30
34
  IconFolderClose16,
31
35
  IconFolderOpenOutline16,
32
36
  IconPanelLeftOutline16,
33
37
  IconProjectAddOutline16,
38
+ IconSettingsOutline16,
39
+ IconShieldOutline16,
40
+ IconWorkflowAgent16,
34
41
  } from "@/components/icons";
35
42
  import { useI18n } from "@/i18n";
36
43
  import { usePiWeb } from "@/hooks/usePiWeb";
@@ -39,7 +46,7 @@ import { useFileViewer } from "@/hooks/useFileViewer";
39
46
  import type { TreeNode } from "@/lib/growth-tree";
40
47
  import { syncPebrelTheme } from "@/lib/theme";
41
48
  import type { ModelChoice } from "@/components/ModelSelector";
42
- import type { ImageAttachment, TrajEntry } from "@/lib/types";
49
+ import type { ImageAttachment, TrajEntry, WorkflowMode } from "@/lib/types";
43
50
 
44
51
  // dsh ui-layout columns.ts 几何常量
45
52
  const SIDEBAR_MIN = 264;
@@ -72,6 +79,8 @@ export function AppShell() {
72
79
  currentPath,
73
80
  state,
74
81
  models,
82
+ modelLoading,
83
+ modelLoadError,
75
84
  addedWorkspaces,
76
85
  removedWorkspaces,
77
86
  workspaceAliases,
@@ -86,9 +95,11 @@ export function AppShell() {
86
95
  setGroupBy,
87
96
  setOrderBy,
88
97
  addWorkspaceByPicker,
98
+ addWorkspaceByPath,
89
99
  removeWorkspace,
90
100
  refreshModels,
91
101
  sendCommand,
102
+ editAndResend,
92
103
  newSession,
93
104
  openSession,
94
105
  closeSession,
@@ -144,6 +155,8 @@ export function AppShell() {
144
155
  const [heroCwd, setHeroCwd] = useState("");
145
156
  const [heroModel, setHeroModel] = useState<{ provider: string; id: string } | null>(null);
146
157
  const [heroThinking, setHeroThinking] = useState("");
158
+ const [heroMode, setHeroMode] = useState<WorkflowMode>("agent");
159
+ const [activeTopView, setActiveTopView] = useState<"workbench" | "diff" | "tests" | "swarm" | "archive">("workbench");
147
160
  const [isNarrow, setIsNarrow] = useState(false);
148
161
  const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
149
162
  useEffect(() => {
@@ -357,15 +370,9 @@ export function AppShell() {
357
370
  const doEditMessage = useCallback(
358
371
  async (entryId: string, text: string) => {
359
372
  if (!currentId) return;
360
- const done = await sendCommand({ cmd: "editAndResend", entryId, text });
361
- if (!done?.success) {
362
- setError(done?.error ?? t.editMessageFailed);
363
- return;
364
- }
365
- // 被编辑消息之后的尾部要从视图里消失(服务端已切断该分支)
366
- resync();
373
+ await editAndResend(entryId, text);
367
374
  },
368
- [currentId, resync, sendCommand, setError, t.editMessageFailed],
375
+ [currentId, editAndResend],
369
376
  );
370
377
 
371
378
  const doArchiveSession = useCallback(
@@ -374,6 +381,7 @@ export function AppShell() {
374
381
  await archiveSession(path);
375
382
  } catch {
376
383
  // usePiWeb exposes the request failure in the shared error banner.
384
+ return;
377
385
  }
378
386
  // 归档后会话立刻离开工作区列表(服务端同时让它收工)。正在看的会话被归档时
379
387
  // 必须切走:先取同一工作区的另一个可见会话,没有就回到空态(工作区仍留在侧栏)。
@@ -432,6 +440,7 @@ export function AppShell() {
432
440
  provider: effective?.provider,
433
441
  modelId: effective?.id,
434
442
  thinking: heroThinking || undefined,
443
+ mode: heroMode,
435
444
  });
436
445
  if (!p) return { success: false };
437
446
  // Bind before the new-session render. In-flight ChatInput callbacks still own
@@ -521,6 +530,8 @@ export function AppShell() {
521
530
 
522
531
  // 项目栏/文件查看器的工作区:当前会话的工作区优先,快照未到时回落到会话列表里的 cwd
523
532
  const panelCwd = snapshot?.cwd || currentSession?.cwd || heroCwd || knownCwds[0] || "";
533
+ const activeSnapshot = currentPath && snapshot?.sessionPath && samePath(snapshot.sessionPath, currentPath) ? snapshot : null;
534
+ const activeWorkspaceCwd = currentPath ? (activeSnapshot?.cwd || currentSession?.cwd || "") : heroCwd;
524
535
 
525
536
  // Git 面板自动刷新:pi 每完成一个会改动文件的工具,或一轮结束时,静默重拉
526
537
  const mutatingDone = useMemo(
@@ -528,6 +539,8 @@ export function AppShell() {
528
539
  [state.tools],
529
540
  );
530
541
  const [panelRefreshKey, setPanelRefreshKey] = useState(0);
542
+ const [gitSummary, setGitSummary] = useState<{ available: boolean; isRepo: boolean; branch: string | null; detached: boolean; files: unknown[]; statusError?: string } | null>(null);
543
+ const [gitSummaryError, setGitSummaryError] = useState(false);
531
544
  const prevMutatingRef = useRef(mutatingDone);
532
545
  const prevStreamingRef = useRef(isStreaming);
533
546
  useEffect(() => {
@@ -540,6 +553,31 @@ export function AppShell() {
540
553
  if (prevStreamingRef.current && !isStreaming) setPanelRefreshKey((k) => k + 1);
541
554
  prevStreamingRef.current = isStreaming;
542
555
  }, [isStreaming]);
556
+ useEffect(() => {
557
+ const controller = new AbortController();
558
+ setGitSummary(null);
559
+ setGitSummaryError(false);
560
+ if (!activeWorkspaceCwd) return () => controller.abort();
561
+ void (async () => {
562
+ try {
563
+ const response = await fetch(`/api/git?cwd=${encodeURIComponent(activeWorkspaceCwd)}`, { signal: controller.signal });
564
+ const body = await response.json();
565
+ if (!response.ok || !body.success) throw new Error(body.error || "Git 状态读取失败");
566
+ if (!controller.signal.aborted) setGitSummary(body.data);
567
+ } catch {
568
+ if (!controller.signal.aborted) setGitSummaryError(true);
569
+ }
570
+ })();
571
+ return () => controller.abort();
572
+ }, [activeWorkspaceCwd, panelRefreshKey]);
573
+ const gitBranchLabel = gitSummary?.isRepo ? (gitSummary.detached ? "HEAD" : gitSummary.branch || "—") : "—";
574
+ const gitStatusLabel = !activeWorkspaceCwd ? "未选择"
575
+ : gitSummaryError || gitSummary?.statusError ? "状态未知"
576
+ : !gitSummary ? "读取中"
577
+ : !gitSummary.available ? "Git 不可用"
578
+ : !gitSummary.isRepo ? "非仓库"
579
+ : gitSummary.files.length ? `${gitSummary.files.length} 改动` : "干净";
580
+ const gitCanCommit = Boolean(gitSummary?.isRepo && !gitSummary.statusError && gitSummary.files.length);
543
581
 
544
582
  const insertIntoComposer = useCallback((text: string) => {
545
583
  const textarea = document.querySelector<HTMLTextAreaElement>('[data-testid="composer"] textarea');
@@ -553,12 +591,12 @@ export function AppShell() {
553
591
  requestAnimationFrame(() => textarea?.focus());
554
592
  }, [draftKey, updateDraft]);
555
593
  const openInEditor = useCallback(
556
- async (filePath: string) => {
594
+ async (filePath: string, cwdOverride?: string) => {
557
595
  try {
558
596
  const r = await fetch("/api/files", {
559
597
  method: "POST",
560
598
  headers: { "Content-Type": "application/json" },
561
- body: JSON.stringify({ action: "open", cwd: panelCwd, path: filePath }),
599
+ body: JSON.stringify({ action: "open", cwd: cwdOverride || panelCwd, path: filePath }),
562
600
  });
563
601
  const j = await r.json();
564
602
  if (!j.success) throw new Error(j.error || "failed to open file");
@@ -635,28 +673,159 @@ export function AppShell() {
635
673
  );
636
674
 
637
675
  return (
638
- <div
639
- className="grid h-screen w-screen overflow-hidden"
640
- style={{
641
- gridTemplateColumns: gridCols,
642
- transition: dragging ? "none" : "grid-template-columns var(--ds-duration-slow) var(--ds-ease-in-out)",
643
- background: "var(--dsw-bg-base)",
644
- }}
645
- >
646
- {/* 侧栏 */}
647
- <div
648
- className="min-h-0 overflow-hidden"
649
- style={isNarrow ? {
650
- position: "fixed",
651
- inset: "0 auto 0 0",
652
- width: "min(86vw, 340px)",
653
- zIndex: 90,
654
- background: "var(--dsw-sidebar-fill)",
655
- transform: mobileSidebarOpen ? "translateX(0)" : "translateX(-105%)",
656
- transition: "transform var(--ds-duration-normal) var(--ds-ease-in-out)",
657
- boxShadow: mobileSidebarOpen ? "var(--dsw-elevation-prominent)" : "none",
658
- } : { background: "var(--dsw-sidebar-fill)" }}
676
+ <div className="flex flex-col h-screen w-screen overflow-hidden bg-clay-bg text-clay-slate-text antialiased">
677
+ {/* 全局粘土顶部导航栏 (Screen 1, 2, 3, 8) */}
678
+ <header className="pw-top-navbar h-14 px-6 flex items-center justify-between border-b border-white/60 bg-white/70 backdrop-blur-md z-30 shrink-0 select-none">
679
+ {/* Left: Engine brand badge + Code Workbench capsule */}
680
+ <div className="flex items-center gap-3 min-w-0">
681
+ <div className="w-9 h-9 clay-badge bg-gradient-to-tr from-emerald-500 to-teal-400 flex items-center justify-center text-white font-black text-lg shadow-sm">
682
+ π
683
+ </div>
684
+ <div className="flex items-baseline gap-2">
685
+ <span className="font-extrabold text-lg tracking-tight text-slate-900 dark:text-slate-100">piweb</span>
686
+ <span className="text-xs font-bold px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-800 dark:bg-emerald-950/70 dark:text-emerald-300 tracking-wide uppercase border border-emerald-300 dark:border-emerald-800">Code Workbench</span>
687
+ </div>
688
+ </div>
689
+
690
+ {/* Center: Session Breadcrumb Status Pill */}
691
+ <div className="hidden lg:flex items-center gap-2 px-3 py-1.5 clay-inset bg-white dark:bg-slate-900 rounded-full text-xs font-medium border border-slate-200 dark:border-slate-800 min-w-0">
692
+ <span className={`w-2.5 h-2.5 rounded-full flex-none ${state.connected ? "bg-emerald-500 animate-pulse" : "bg-slate-400"}`} />
693
+ <span className="font-bold text-slate-800 dark:text-slate-200">PiWeb</span>
694
+ <span className="text-slate-400">/</span>
695
+ <span className="font-semibold text-slate-600 dark:text-slate-400">SESSION</span>
696
+ <span className="text-slate-400">/</span>
697
+ <span className="text-slate-900 dark:text-slate-100 max-w-xs truncate font-bold">
698
+ {currentId ? title : (heroCwd ? (getWorkspaceName ? getWorkspaceName(heroCwd) : basename(heroCwd)) : t.newChat)}
699
+ </span>
700
+ </div>
701
+
702
+ {/* Center/Nav: View Switcher Pill Tabs */}
703
+ <nav aria-label="视图切换" className="clay-inset flex min-w-0 items-center overflow-x-auto p-1 rounded-full bg-slate-100 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 gap-1 text-xs font-semibold">
704
+ <button
705
+ type="button"
706
+ className={`px-3.5 py-1 rounded-full transition-all duration-150 ${
707
+ activeTopView === "workbench"
708
+ ? "clay-btn-peach text-white font-bold shadow-sm"
709
+ : "text-slate-700 hover:text-slate-950 dark:text-slate-300 dark:hover:text-white"
710
+ }`}
711
+ onClick={() => setActiveTopView("workbench")}
712
+ >
713
+ 工作台
714
+ </button>
715
+ <button
716
+ type="button"
717
+ className={`flex items-center gap-1.5 px-3.5 py-1 rounded-full transition-all duration-150 ${
718
+ activeTopView === "diff"
719
+ ? "clay-btn-peach text-white font-bold shadow-sm"
720
+ : "text-slate-700 hover:text-slate-950 dark:text-slate-300 dark:hover:text-white"
721
+ }`}
722
+ onClick={() => { setPanelRefreshKey((key) => key + 1); setActiveTopView("diff"); }}
723
+ >
724
+ <span>差异对比</span>
725
+ <span className={`text-[10px] px-1.5 py-0.2 rounded-full font-bold ${
726
+ activeTopView === "diff" ? "bg-white/30 text-white" : "bg-emerald-100 text-emerald-800 dark:bg-emerald-950/70 dark:text-emerald-300"
727
+ }`}>
728
+ {gitSummary?.isRepo ? gitSummary.files.length : "—"}
729
+ </span>
730
+ </button>
731
+ <button
732
+ type="button"
733
+ className={`shrink-0 px-3.5 py-1 rounded-full transition-all duration-150 ${
734
+ activeTopView === "tests"
735
+ ? "clay-btn-peach text-white font-bold shadow-sm"
736
+ : "text-slate-700 hover:text-slate-950 dark:text-slate-300 dark:hover:text-white"
737
+ }`}
738
+ onClick={() => setActiveTopView("tests")}
739
+ >
740
+ 测试检查器
741
+ </button>
742
+ <button
743
+ type="button"
744
+ className={`flex items-center gap-1.5 px-3.5 py-1 rounded-full transition-all duration-150 ${
745
+ activeTopView === "swarm"
746
+ ? "clay-btn-peach text-white font-bold shadow-sm"
747
+ : "text-slate-700 hover:text-slate-950 dark:text-slate-300 dark:hover:text-white"
748
+ }`}
749
+ onClick={() => setActiveTopView("swarm")}
750
+ >
751
+ <span>智能体群</span>
752
+ </button>
753
+ </nav>
754
+
755
+ {/* Right: Actions */}
756
+ <div className="flex items-center gap-2">
757
+ <button
758
+ type="button"
759
+ className="clay-badge hidden md:flex items-center gap-1.5 px-3 py-1 text-xs font-mono text-slate-800 dark:text-slate-200 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 shadow-sm font-semibold hover:border-slate-300 transition-all cursor-pointer"
760
+ title="Git 工作区状态(点击进入差异对比)"
761
+ onClick={() => {
762
+ setPanelRefreshKey((key) => key + 1);
763
+ setActiveTopView("diff");
764
+ }}
765
+ >
766
+ <IconBranchOutline16 size={13} className="text-emerald-500" />
767
+ <span>{gitBranchLabel}</span>
768
+ <span className="text-slate-400">·</span>
769
+ <span className="text-amber-600 dark:text-amber-400 font-bold">{gitStatusLabel}</span>
770
+ </button>
771
+ <button
772
+ type="button"
773
+ className="w-8 h-8 clay-btn bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white transition-colors"
774
+ title={t.projectPanel}
775
+ aria-label={t.projectPanel}
776
+ data-testid="project-toggle"
777
+ onClick={() => toggleProject()}
778
+ >
779
+ <IconFolderOpenOutline16 size={15} />
780
+ </button>
781
+ <button
782
+ type="button"
783
+ className="w-8 h-8 clay-btn bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white transition-colors"
784
+ title={t.promptPanel}
785
+ aria-label={t.promptPanel}
786
+ data-testid="prompt-panel-toggle"
787
+ onClick={() => {
788
+ setPromptsOpen((open) => !open);
789
+ setSelected(null);
790
+ setGitDetailsOpen(false);
791
+ }}
792
+ >
793
+ <IconAgentPresetOutline16 size={15} />
794
+ </button>
795
+ <button
796
+ type="button"
797
+ className="w-8 h-8 clay-btn bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white transition-colors"
798
+ title={t.settings}
799
+ aria-label={t.settings}
800
+ data-testid="settings-toggle"
801
+ onClick={() => setSettingsOpen(true)}
802
+ >
803
+ <IconSettingsOutline16 size={15} />
804
+ </button>
805
+ </div>
806
+ </header>
807
+
808
+ {/* 三栏/四栏悬浮粘土卡片主体容器 */}
809
+ <main
810
+ className="pw-shell grid flex-1 min-h-0 overflow-hidden p-3 gap-3 bg-clay-bg"
811
+ style={{
812
+ gridTemplateColumns: gridCols,
813
+ transition: dragging ? "none" : "grid-template-columns var(--ds-duration-slow) var(--ds-ease-in-out)",
814
+ }}
659
815
  >
816
+ {/* 侧栏卡片 */}
817
+ <div
818
+ className="pw-sidebar-surface clay-surface rounded-3xl p-3 shrink-0 flex flex-col justify-between overflow-hidden shadow-md"
819
+ style={isNarrow ? {
820
+ position: "fixed",
821
+ inset: "0 auto 0 0",
822
+ width: "min(86vw, 340px)",
823
+ zIndex: 90,
824
+ transform: mobileSidebarOpen ? "translateX(0)" : "translateX(-105%)",
825
+ transition: "transform var(--ds-duration-normal) var(--ds-ease-in-out)",
826
+ boxShadow: mobileSidebarOpen ? "var(--dsw-elevation-prominent)" : "none",
827
+ } : undefined}
828
+ >
660
829
  <SessionSidebar
661
830
  sessions={sessions}
662
831
  sessionListError={sessionListError}
@@ -701,15 +870,104 @@ export function AppShell() {
701
870
  }}
702
871
  onAddWorkspace={() => void addWorkspaceByPicker()}
703
872
  draftCwd={!currentId ? heroCwd || null : null}
873
+ gitBranch={gitBranchLabel}
874
+ gitStatus={gitStatusLabel}
875
+ canAskCommit={gitCanCommit}
876
+ onAskCommit={() => { setActiveTopView("workbench"); insertIntoComposer(t.gitAskCommitPrompt); }}
877
+ activeTopView={activeTopView}
878
+ onSelectTopView={(v) => { setActiveTopView(v); setMobileSidebarOpen(false); }}
704
879
  />
705
880
  </div>
706
881
 
707
- {/* 四列网格中始终保留项目列,否则关闭项目栏时会话区会落进 0px 列。 */}
708
- {!isNarrow && (projectColumn || <div aria-hidden="true" />)}
882
+ {/* 四列网格中始终保留项目列 */}
883
+ {!isNarrow && (projectColumn ? (
884
+ <div className="clay-surface rounded-3xl p-3 shrink-0 flex flex-col overflow-hidden shadow-md">
885
+ {projectColumn}
886
+ </div>
887
+ ) : <div aria-hidden="true" />)}
709
888
 
710
- {/* 会话区 */}
711
- <div className="pi-main flex min-h-0 min-w-0 flex-col">
712
- {!currentId ? (
889
+ {/* 会话区与多视图卡片 (Screen 1 & Screen 3) */}
890
+ <section className={`pi-main pw-main flex flex-col clay-card rounded-3xl overflow-hidden min-h-0 min-w-0 relative bg-white shadow-md${currentId ? "" : " pw-main-hero"}`}>
891
+
892
+
893
+ {/* 视图内容切换 */}
894
+ {activeTopView === "diff" ? (
895
+ <div className="flex-1 min-h-0 overflow-hidden p-4 sm:p-6 bg-slate-50/50 dark:bg-slate-950/30">
896
+ <div className="clay-card mx-auto h-full max-w-6xl overflow-hidden rounded-3xl bg-white/85 dark:bg-slate-900/80">
897
+ <GitPanel cwd={activeWorkspaceCwd} refreshKey={panelRefreshKey} onClose={() => setActiveTopView("workbench")} onOpenFile={(filePath) => void openInEditor(filePath, activeWorkspaceCwd)} />
898
+ </div>
899
+ </div>
900
+ ) : activeTopView === "tests" ? (
901
+ <div className="flex-1 min-h-0 overflow-y-auto p-4 sm:p-6 bg-slate-50/50 dark:bg-slate-950/30">
902
+ <TestInspectorPod
903
+ cwd={activeWorkspaceCwd}
904
+ onClose={() => setActiveTopView("workbench")}
905
+ onSwitchToWorkbench={() => setActiveTopView("workbench")}
906
+ onSwitchToDiff={() => setActiveTopView("diff")}
907
+ />
908
+ </div>
909
+ ) : activeTopView === "swarm" ? (
910
+ <div className="flex-1 min-h-0 overflow-hidden p-3 sm:p-4 bg-slate-50/50 dark:bg-slate-950/30">
911
+ <SwarmCoordinatorPod
912
+ cwd={activeWorkspaceCwd}
913
+ onClose={() => setActiveTopView("workbench")}
914
+ onSwitchToWorkbench={() => setActiveTopView("workbench")}
915
+ projectTrust={activeSnapshot?.projectTrust}
916
+ onWorkspaceChanged={() => setPanelRefreshKey((key) => key + 1)}
917
+ />
918
+ </div>
919
+ ) : activeTopView === "archive" ? (
920
+ <div className="flex-1 min-h-0 overflow-y-auto p-6 flex flex-col items-center bg-slate-50/40 dark:bg-slate-950/20">
921
+ <div className="clay-card max-w-2xl w-full p-6 rounded-3xl space-y-4">
922
+ <div className="flex items-center justify-between pb-3 border-b border-slate-200/60 dark:border-slate-800/60">
923
+ <h2 className="text-base font-bold text-slate-800 dark:text-slate-100 flex items-center gap-2">
924
+ <IconArchiveOutline20 size={18} className="text-slate-400" />
925
+ 已归档会话 ({archivedSessions.length})
926
+ </h2>
927
+ <span className="text-xs text-slate-400">保留历史记录,不占用主工作台</span>
928
+ </div>
929
+ {archivedSessions.length === 0 ? (
930
+ <div className="text-center py-12 text-sm text-slate-400">暂无归档会话</div>
931
+ ) : (
932
+ <div className="space-y-2">
933
+ {archivedSessions.map((session) => (
934
+ <div
935
+ key={session.path}
936
+ className="clay-card flex items-center justify-between p-3.5 rounded-2xl hover:scale-[1.01] transition-transform"
937
+ >
938
+ <div className="min-w-0 flex-1">
939
+ <div className="font-semibold text-sm text-slate-800 dark:text-slate-200 truncate">{session.name || session.firstMessage || basename(session.path)}</div>
940
+ <div className="text-xs text-slate-400 truncate mt-0.5">{session.cwd}</div>
941
+ </div>
942
+ <div className="flex items-center gap-2 ml-4">
943
+ <button
944
+ type="button"
945
+ className="clay-btn clay-btn-soft px-3 py-1.5 text-xs text-slate-700 dark:text-slate-200"
946
+ onClick={() => {
947
+ doUnarchiveSession(session.path);
948
+ setActiveTopView("workbench");
949
+ }}
950
+ >
951
+ 恢复会话
952
+ </button>
953
+ <button
954
+ type="button"
955
+ className="clay-btn clay-btn-peach px-3 py-1.5 text-xs text-white"
956
+ onClick={() => {
957
+ openSession(session.path);
958
+ setActiveTopView("workbench");
959
+ }}
960
+ >
961
+ 查看
962
+ </button>
963
+ </div>
964
+ </div>
965
+ ))}
966
+ </div>
967
+ )}
968
+ </div>
969
+ </div>
970
+ ) : !currentId ? (
713
971
  <div className="flex min-h-0 flex-1 flex-col">
714
972
  <Hero
715
973
  draft={composerDrafts.current.get(heroDraftKey) ?? EMPTY_CHAT_DRAFT}
@@ -728,6 +986,9 @@ export function AppShell() {
728
986
  getWorkspaceName={getWorkspaceName}
729
987
  onSend={heroSend}
730
988
  models={modelChoices}
989
+ modelLoading={modelLoading}
990
+ modelLoadError={modelLoadError}
991
+ onRetryModels={() => void refreshModels()}
731
992
  providerNames={providerNames}
732
993
  authByProvider={authByProvider}
733
994
  addWorkspaceByPicker={addWorkspaceByPicker}
@@ -739,8 +1000,11 @@ export function AppShell() {
739
1000
  }}
740
1001
  defaultModel={defaultModel}
741
1002
  heroModelLevels={heroModelLevels}
742
- heroThinking={heroThinking}
743
- onSelectHeroThinking={setHeroThinking}
1003
+ heroThinking={heroThinking}
1004
+ onSelectHeroThinking={setHeroThinking}
1005
+ heroMode={heroMode}
1006
+ onSelectHeroMode={setHeroMode}
1007
+ onOpenSwarm={() => setActiveTopView("swarm")}
744
1008
  />
745
1009
  {/* 草稿阶段的错误行内显示(不再弹右下角) */}
746
1010
  {state.error && (
@@ -758,43 +1022,9 @@ export function AppShell() {
758
1022
  </div>
759
1023
  ) : (
760
1024
  <>
761
- {/* 头部:标题行 + 页签行(dsh 两行式) */}
762
- <div className="hairline-b px-5 pb-0 pt-3">
763
- <div className="flex items-center gap-3">
764
- <span className="min-w-0 flex-1 truncate" style={{ fontSize: 14.5, fontWeight: 600 }}>
765
- {title}
766
- </span>
767
- {/* 项目生长入口;轨迹详情由对话内工具行点击唤起 */}
768
- <button
769
- type="button"
770
- className="icon-btn"
771
- style={{ width: 30, height: 30, background: projectOpen ? "var(--dsw-active)" : undefined }}
772
- title={`${t.projectPanel} (Ctrl/⌘+Shift+E)`}
773
- aria-label={t.projectPanel}
774
- data-testid="project-toggle"
775
- onClick={() => toggleProject()}
776
- >
777
- <IconFolderOpenOutline16 size={15} />
778
- </button>
779
- {/* 提示词来源:系统提示词 / 记忆 / 技能 / 模板 / 工具提示词集中列出,点开即看 */}
780
- <button
781
- type="button"
782
- className="icon-btn"
783
- style={{ width: 30, height: 30, background: promptsOpen ? "var(--dsw-active)" : undefined }}
784
- title={t.promptPanel}
785
- aria-label={t.promptPanel}
786
- data-testid="prompt-panel-toggle"
787
- onClick={() => {
788
- setPromptsOpen((open) => !open);
789
- setSelected(null);
790
- setGitDetailsOpen(false);
791
- }}
792
- >
793
- <IconAgentPresetOutline16 size={15} />
794
- </button>
795
- </div>
1025
+ {/* 会话信任横幅 */}
796
1026
  {projectTrust?.required && !projectTrust.trusted && (
797
- <div className="mt-2 flex flex-wrap items-center gap-2 rounded-xl px-3 py-2" style={{ fontSize: 12.5, background: "var(--dsw-hover)", border: "0.5px solid var(--dsw-border-l2)" }} role="status">
1027
+ <div className="mx-5 mt-2 flex flex-wrap items-center gap-2 rounded-xl px-3 py-2" style={{ fontSize: 12.5, background: "var(--dsw-hover)", border: "0.5px solid var(--dsw-border-l2)" }} role="status">
798
1028
  <span style={{ color: "var(--dsw-warn)", fontWeight: 600 }}>⚠</span>
799
1029
  <span className="min-w-0 flex-1">{projectTrust.source === "default-never" || projectTrust.source === "remembered" ? t.projectTrustBannerNever : t.projectTrustBanner}</span>
800
1030
  <button className="btn-primary-white" style={{ height: 26, padding: "0 10px", fontSize: 12 }} onClick={() => void setProjectTrust(true)}>{t.projectTrustAllow}</button>
@@ -803,8 +1033,6 @@ export function AppShell() {
803
1033
  )}
804
1034
  </div>
805
1035
  )}
806
- {/* 轨迹页签已移除:轨迹按需查看(对话里的工具行 → 轨迹详情),不再占用主区域 */}
807
- </div>
808
1036
 
809
1037
  {/* 内容:只有对话区,轨迹按需在详情栏查看 */}
810
1038
  {!snapshot ? (
@@ -846,19 +1074,19 @@ export function AppShell() {
846
1074
  onFork={handleFork}
847
1075
  onEditMessage={doEditMessage}
848
1076
  />
849
- <div className="px-4 pb-3 pt-2">
1077
+ <div className="pw-session-composer px-4 pb-3 pt-2">
850
1078
  <div className="mx-auto w-full" style={{ maxWidth: "var(--dsh-composer-card-max-width)" }}>
851
1079
  {/* 运行状态指示由 ChatWindow 内的 WorkingIndicator 承担(含工具/输出 token 信息) */}
852
1080
  <ChatInput
853
1081
  key={draftKey}
854
- draft={composerDrafts.current.get(draftKey) ?? EMPTY_CHAT_DRAFT}
855
- onDraftChange={saveSessionDraft}
856
- onUploadError={(message) => failUpload(draftKey, message)}
857
- onUploadProgress={(delta) => trackUpload(draftKey, delta)}
858
- uploadFailure={uploadFailures[draftKey]}
859
- pendingUploadCount={uploadCounts[draftKey] ?? 0}
860
- pendingSend={pendingSends[draftKey] ?? false}
861
- onSendPendingChange={(pending) => trackSend(draftKey, pending)}
1082
+ draft={composerDrafts.current.get(draftKey) ?? EMPTY_CHAT_DRAFT}
1083
+ onDraftChange={saveSessionDraft}
1084
+ onUploadError={(message) => failUpload(draftKey, message)}
1085
+ onUploadProgress={(delta) => trackUpload(draftKey, delta)}
1086
+ uploadFailure={uploadFailures[draftKey]}
1087
+ pendingUploadCount={uploadCounts[draftKey] ?? 0}
1088
+ pendingSend={pendingSends[draftKey] ?? false}
1089
+ onSendPendingChange={(pending) => trackSend(draftKey, pending)}
862
1090
  isStreaming={isStreaming}
863
1091
  contextPercent={snapshot?.contextUsage?.percent ?? null}
864
1092
  contextTokens={snapshot?.contextUsage?.tokens ?? null}
@@ -869,9 +1097,15 @@ export function AppShell() {
869
1097
  thinkingLevel={snapshot?.thinkingLevel}
870
1098
  thinkingLevels={snapshot?.thinkingLevels ?? ["off", "minimal", "low", "medium", "high", "xhigh", "max"]}
871
1099
  models={modelChoices}
1100
+ modelLoading={modelLoading}
1101
+ modelLoadError={modelLoadError}
1102
+ onRetryModels={() => void refreshModels()}
872
1103
  providerNames={providerNames}
873
1104
  authByProvider={authByProvider}
874
1105
  queue={snapshot?.queue ?? { steering: [], followUp: [] }}
1106
+ workflow={snapshot?.workflow}
1107
+ onWorkflowModeChange={(mode) => sendCommand({ cmd: "setWorkflowMode", mode })}
1108
+ onApprovePlan={() => sendCommand({ cmd: "approvePlan" })}
875
1109
  commands={slashCommands}
876
1110
  onCommand={runSlashCommand}
877
1111
  onSend={sendPrompt}
@@ -893,45 +1127,46 @@ export function AppShell() {
893
1127
  {Object.entries(state.extensionStatuses).map(([k, v]) => <span key={k}>{v}</span>)}
894
1128
  </div>
895
1129
  )}
896
- </div>
1130
+ </div>
897
1131
  </div>
898
1132
  </div>
899
1133
  )
900
1134
  }
901
1135
  </>
902
1136
  )}
903
- </div>
1137
+ </section>
904
1138
 
905
- {/* 提示词来源 / 轨迹 / Git 详情栏 */}
906
- {(selected || gitDetailsOpen || promptsOpen) && (
907
- <div
908
- className="flex min-h-0 flex-col overflow-hidden"
909
- style={isNarrow ? {
910
- position: "fixed",
911
- inset: "0 0 0 auto",
912
- width: "min(92vw, 640px)",
913
- zIndex: 90,
914
- background: "var(--dsw-sidebar-fill)",
915
- boxShadow: "var(--dsw-elevation-prominent)",
916
- } : { background: "var(--dsw-sidebar-fill)", borderLeft: "0.5px solid var(--dsw-border-l2)" }}
917
- >
918
- <div className="min-h-0 flex-1">
919
- {promptsOpen ? (
920
- <PromptPanel
921
- cwd={panelCwd}
922
- sessionId={currentId}
923
- refreshKey={currentId}
924
- onOpenContent={(path, content) => viewer.openStatic(path, content)}
925
- onClose={() => setPromptsOpen(false)}
926
- />
927
- ) : selected ? (
928
- <TrajInspector entry={selected} onClose={() => setSelected(null)} />
929
- ) : (
930
- <GitPanel cwd={panelCwd} refreshKey={panelRefreshKey} onClose={() => setGitDetailsOpen(false)} onAskCommit={() => insertIntoComposer(t.gitAskCommitPrompt)} onOpenFile={openInEditor} />
931
- )}
932
- </div>
933
- </div>
934
- )}
1139
+ {/* 提示词来源 / 轨迹 / Git 详情栏卡片 */}
1140
+ {(selected || gitDetailsOpen || promptsOpen) && (
1141
+ <aside
1142
+ className="pw-details-surface clay-surface rounded-3xl p-4 shrink-0 flex flex-col overflow-hidden shadow-md"
1143
+ data-purpose="inspector-panel"
1144
+ style={isNarrow ? {
1145
+ position: "fixed",
1146
+ inset: "0 0 0 auto",
1147
+ width: "min(92vw, 640px)",
1148
+ zIndex: 90,
1149
+ boxShadow: "var(--dsw-elevation-prominent)",
1150
+ } : undefined}
1151
+ >
1152
+ <div className="min-h-0 flex-1 flex flex-col">
1153
+ {promptsOpen ? (
1154
+ <PromptPanel
1155
+ cwd={panelCwd}
1156
+ sessionId={currentId}
1157
+ refreshKey={currentId}
1158
+ onOpenContent={(path, content) => viewer.openStatic(path, content)}
1159
+ onClose={() => setPromptsOpen(false)}
1160
+ />
1161
+ ) : selected ? (
1162
+ <TrajInspector entry={selected} onClose={() => setSelected(null)} />
1163
+ ) : (
1164
+ <GitPanel cwd={panelCwd} refreshKey={panelRefreshKey} onClose={() => setGitDetailsOpen(false)} onAskCommit={() => insertIntoComposer(t.gitAskCommitPrompt)} onOpenFile={openInEditor} />
1165
+ )}
1166
+ </div>
1167
+ </aside>
1168
+ )}
1169
+ </main>
935
1170
 
936
1171
  {/* 拖拽手柄 */}
937
1172
  {!isNarrow && !sidebarCollapsed && (
@@ -1045,6 +1280,9 @@ function Hero({
1045
1280
  getWorkspaceName,
1046
1281
  onSend,
1047
1282
  models,
1283
+ modelLoading,
1284
+ modelLoadError,
1285
+ onRetryModels,
1048
1286
  providerNames,
1049
1287
  authByProvider,
1050
1288
  addWorkspaceByPicker,
@@ -1054,6 +1292,9 @@ function Hero({
1054
1292
  heroModelLevels,
1055
1293
  heroThinking,
1056
1294
  onSelectHeroThinking,
1295
+ heroMode,
1296
+ onSelectHeroMode,
1297
+ onOpenSwarm,
1057
1298
  }: {
1058
1299
  draft: ChatDraft;
1059
1300
  onDraftChange: (update: ChatDraftUpdate) => void;
@@ -1071,6 +1312,9 @@ function Hero({
1071
1312
  getWorkspaceName?: (cwd: string) => string;
1072
1313
  onSend: (text: string, images: ImageAttachment[]) => Promise<{ success: boolean }>;
1073
1314
  models: ModelChoice[];
1315
+ modelLoading: boolean;
1316
+ modelLoadError: string | null;
1317
+ onRetryModels: () => void;
1074
1318
  providerNames: Record<string, string>;
1075
1319
  authByProvider: Record<string, boolean>;
1076
1320
  addWorkspaceByPicker: () => Promise<string | null>;
@@ -1080,6 +1324,9 @@ function Hero({
1080
1324
  heroModelLevels: string[];
1081
1325
  heroThinking: string;
1082
1326
  onSelectHeroThinking: (level: string) => void;
1327
+ heroMode: WorkflowMode;
1328
+ onSelectHeroMode: (mode: WorkflowMode) => void;
1329
+ onOpenSwarm?: () => void;
1083
1330
  }) {
1084
1331
  const { t } = useI18n();
1085
1332
  const [wsMenu, setWsMenu] = useState(false);
@@ -1097,81 +1344,106 @@ function Hero({
1097
1344
  }, [wsMenu]);
1098
1345
 
1099
1346
  const pickWorkspace = async () => {
1347
+ setWsMenu(false);
1100
1348
  const p = await addWorkspaceByPicker();
1101
1349
  if (p) {
1102
1350
  setCwd(p);
1103
- setWsMenu(false);
1104
1351
  }
1105
1352
  };
1106
1353
 
1107
1354
  const currentLabel = cwd ? (getWorkspaceName ? getWorkspaceName(cwd) : basename(cwd)) : t.startWith;
1108
1355
 
1109
1356
  return (
1110
- <div className="flex h-full min-h-0 flex-col items-center justify-center px-6">
1111
- {/* 品牌行:仅 π 标 */}
1112
- <div className="mb-5 flex items-center justify-center gap-3">
1113
- <PiMark size={40} />
1114
- </div>
1357
+ <div className="pw-hero flex h-full min-h-0 flex-col items-center justify-center px-6 py-8 overflow-y-auto relative">
1358
+ {/* Ambient orbs */}
1359
+ <div className="absolute -top-10 left-12 w-64 h-64 bg-emerald-200/40 dark:bg-emerald-950/20 rounded-full filter blur-3xl opacity-60 pointer-events-none" />
1360
+ <div className="absolute top-1/3 -right-16 w-80 h-80 bg-sky-200/40 dark:bg-sky-950/20 rounded-full filter blur-3xl opacity-60 pointer-events-none" />
1361
+ <div className="absolute bottom-6 left-1/4 w-72 h-72 bg-orange-200/30 dark:bg-orange-950/20 rounded-full filter blur-3xl opacity-50 pointer-events-none" />
1115
1362
 
1116
- {/* 工作区芯片行 + 输入卡 同宽容器(芯片行与卡片左对齐) */}
1117
- <div className="w-full flex flex-col items-stretch mx-auto" style={{ maxWidth: "var(--dsh-composer-card-max-width)" }}>
1118
- <div ref={menuRef} className="mb-3 flex items-center gap-3" style={{ paddingLeft: 7 }}>
1119
- <div className="relative">
1120
- <button className="hero-chip" data-open={wsMenu} onClick={() => setWsMenu((v) => !v)}>
1121
- <IconFolderClose16 className="hero-chip-icon" size={16} />
1122
- <span className="max-w-[220px] truncate" suppressHydrationWarning>{currentLabel}</span>
1123
- <span className="chevron">
1124
- <IconChevronDown14 size={14} />
1125
- </span>
1126
- </button>
1127
- {wsMenu && (
1128
- <div className="popover absolute bottom-9 left-0 z-50 w-56 py-1">
1129
- {knownCwds.map((c) => {
1130
- const itemLabel = getWorkspaceName ? getWorkspaceName(c) : basename(c);
1131
- return (
1132
- <button
1133
- key={c}
1134
- className="flex w-full items-center gap-2.5 px-3.5 py-2 text-left transition-colors"
1135
- style={{
1136
- fontSize: 13,
1137
- background: cwd === c ? "var(--dsw-accent-soft)" : "transparent",
1138
- color: cwd === c ? "var(--dsw-accent)" : "inherit",
1139
- }}
1140
- onMouseEnter={(e) => {
1141
- if (cwd !== c) e.currentTarget.style.background = "var(--dsw-hover)";
1142
- }}
1143
- onMouseLeave={(e) => {
1144
- if (cwd !== c) e.currentTarget.style.background = "transparent";
1145
- }}
1146
- onClick={() => {
1147
- setCwd(c);
1148
- setWsMenu(false);
1149
- }}
1150
- >
1151
- <IconFolderClose16 size={14} style={{ flex: "none", color: cwd === c ? "var(--dsw-accent)" : "var(--dsw-label-tertiary)" }} />
1152
- <span className="min-w-0 flex-1 truncate">{itemLabel}</span>
1153
- {cwd === c && <IconCheckOutline14 size={13} style={{ flex: "none" }} />}
1154
- </button>
1155
- );
1156
- })}
1157
- <div className="my-1" style={{ borderTop: "0.5px solid var(--dsw-border-l2)" }} />
1158
- <button
1159
- className="flex w-full items-center gap-2.5 px-3.5 py-2 text-left transition-colors"
1160
- style={{ fontSize: 13 }}
1161
- onMouseEnter={(e) => (e.currentTarget.style.background = "var(--dsw-hover)")}
1162
- onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
1163
- onClick={pickWorkspace}
1164
- >
1165
- <IconProjectAddOutline16 size={14} style={{ flex: "none", color: "var(--dsw-label-tertiary)" }} />
1166
- {t.addWorkspace}
1167
- </button>
1168
- </div>
1169
- )}
1363
+ {/* 工作区芯片行 + 输入卡 同宽容器 */}
1364
+ <div className="pw-hero-content w-full flex flex-col items-stretch mx-auto my-auto z-10" style={{ maxWidth: "var(--dsh-composer-card-max-width)" }}>
1365
+ <div className="pw-hero-intro mb-4 text-center">
1366
+ <div className="flex items-center justify-center gap-4 px-4 py-2 rounded-full clay-card mb-3 mx-auto w-fit animate-bounce duration-1000 border border-slate-200 dark:border-slate-800">
1367
+ <div className="w-8 h-8 rounded-full bg-emerald-600 flex items-center justify-center shadow-[inset_1px_1px_2px_rgba(255,255,255,0.7)] text-white font-mono font-bold text-sm">
1368
+ π
1369
+ </div>
1370
+ <div className="flex items-center gap-2">
1371
+ <span className="font-extrabold tracking-wider text-slate-900 dark:text-slate-100 text-xs sm:text-sm">PIWEB</span>
1372
+ <span className="text-slate-400">/</span>
1373
+ <span className="text-[10px] tracking-widest text-slate-600 dark:text-slate-400 font-bold uppercase">WORKBENCH</span>
1374
+ </div>
1375
+ <span className="w-1.5 h-1.5 rounded-full bg-slate-300 dark:bg-slate-600" />
1376
+ <div className="flex items-center gap-1.5 px-2.5 py-0.5 rounded-full clay-inset border border-slate-200 dark:border-slate-800">
1377
+ <span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
1378
+ <span className="font-mono text-[11px] text-emerald-700 dark:text-emerald-300 font-bold tracking-wider">01 / READY</span>
1379
+ </div>
1380
+ </div>
1381
+ <h1 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-slate-900 dark:text-slate-50 mb-2 drop-shadow-sm">
1382
+ 让想法落到代码。
1383
+ </h1>
1384
+ <p className="text-sm text-slate-600 dark:text-slate-300 max-w-lg mx-auto leading-relaxed font-medium">
1385
+ 规划、执行、验证。一个专注于交付的轻量工作台。
1386
+ </p>
1387
+
1388
+ {/* 工作区选择器 */}
1389
+ <div ref={menuRef} className="pw-hero-workspace mt-4 mb-3 flex items-center justify-center gap-2.5">
1390
+ <div className="relative">
1391
+ <button
1392
+ type="button"
1393
+ className="clay-btn clay-btn-soft flex items-center gap-2 px-3.5 py-1.5 rounded-full text-xs font-bold text-slate-800 dark:text-slate-100 border border-slate-200 dark:border-slate-700 shadow-sm"
1394
+ data-open={wsMenu}
1395
+ onClick={() => setWsMenu((v) => !v)}
1396
+ >
1397
+ <IconFolderClose16 className="text-amber-500 flex-none" size={15} />
1398
+ <span className="max-w-[240px] truncate" suppressHydrationWarning>{currentLabel}</span>
1399
+ <IconChevronDown14 size={13} className="text-slate-500 dark:text-slate-400" />
1400
+ </button>
1401
+ {wsMenu && (
1402
+ <div className="clay-card popover absolute top-full left-1/2 -translate-x-1/2 z-50 w-64 mt-2 p-1.5 rounded-2xl shadow-xl border border-slate-200 dark:border-slate-800">
1403
+ <div className="px-3 py-1.5 text-[11px] font-bold text-slate-500 dark:text-slate-400">选择工作区</div>
1404
+ {knownCwds.map((c) => {
1405
+ const itemLabel = getWorkspaceName ? getWorkspaceName(c) : basename(c);
1406
+ return (
1407
+ <button
1408
+ key={c}
1409
+ className="flex w-full items-center gap-2.5 px-3 py-2 text-left rounded-xl transition-colors text-xs font-medium"
1410
+ style={{
1411
+ background: cwd === c ? "var(--dsw-accent-soft)" : "transparent",
1412
+ color: cwd === c ? "var(--dsw-accent)" : "inherit",
1413
+ }}
1414
+ onClick={() => {
1415
+ setCwd(c);
1416
+ setWsMenu(false);
1417
+ }}
1418
+ >
1419
+ <IconFolderClose16 size={14} style={{ flex: "none", color: cwd === c ? "var(--dsw-accent)" : "var(--dsw-label-tertiary)" }} />
1420
+ <span className="min-w-0 flex-1 truncate font-medium">{itemLabel}</span>
1421
+ {cwd === c && <IconCheckOutline14 size={13} style={{ flex: "none" }} />}
1422
+ </button>
1423
+ );
1424
+ })}
1425
+ <div className="my-1 border-t border-slate-200/50 dark:border-slate-800/50" />
1426
+ <button
1427
+ type="button"
1428
+ className="flex w-full items-center gap-2.5 px-3 py-2 text-left rounded-xl transition-colors text-xs text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 font-semibold"
1429
+ onClick={pickWorkspace}
1430
+ >
1431
+ <IconProjectAddOutline16 size={14} style={{ flex: "none", color: "var(--dsw-label-tertiary)" }} />
1432
+ {t.addWorkspace}
1433
+ </button>
1434
+ </div>
1435
+ )}
1436
+ </div>
1437
+ <div className="px-3 py-1.5 rounded-full clay-inset text-slate-700 dark:text-slate-300 text-xs font-mono font-semibold flex items-center gap-1.5 border border-slate-200 dark:border-slate-800">
1438
+ <span className="text-amber-500">⚡</span>
1439
+ <span>在当前工作区执行</span>
1440
+ </div>
1170
1441
  </div>
1171
1442
  </div>
1172
1443
 
1173
1444
  {/* 输入卡 */}
1174
- <ChatInput
1445
+ <div className="w-full mb-4">
1446
+ <ChatInput
1175
1447
  draft={draft}
1176
1448
  onDraftChange={onDraftChange}
1177
1449
  onUploadError={onUploadError}
@@ -1180,26 +1452,60 @@ function Hero({
1180
1452
  pendingUploadCount={pendingUploadCount}
1181
1453
  pendingSend={pendingSend}
1182
1454
  onSendPendingChange={onSendPendingChange}
1183
- commands={commands}
1184
- onCommand={onCommand}
1185
- isStreaming={false}
1186
- contextPercent={null}
1187
- contextTokens={null}
1188
- contextWindow={null}
1189
- contextVisible={false}
1190
- model={heroModel ? models.find((m) => m.provider === heroModel.provider && m.id === heroModel.id) ?? { provider: heroModel.provider, id: heroModel.id, name: heroModel.id, reasoning: false, contextWindow: 0 } : defaultModel}
1191
- thinkingLevel={heroThinking || undefined}
1192
- thinkingLevels={heroModelLevels}
1193
- models={models}
1194
- providerNames={providerNames}
1195
- authByProvider={authByProvider}
1196
- queue={{ steering: [], followUp: [] }}
1197
- onSend={onSend}
1198
- onSteer={() => ({ success: false })}
1199
- onAbort={() => {}}
1200
- onSelectModel={onSelectHeroModel}
1201
- onSelectLevel={onSelectHeroThinking}
1202
- />
1455
+ commands={commands}
1456
+ onCommand={onCommand}
1457
+ isStreaming={false}
1458
+ contextPercent={null}
1459
+ contextTokens={null}
1460
+ contextWindow={null}
1461
+ contextVisible={false}
1462
+ model={heroModel ? models.find((m) => m.provider === heroModel.provider && m.id === heroModel.id) ?? { provider: heroModel.provider, id: heroModel.id, name: heroModel.id, reasoning: false, contextWindow: 0 } : defaultModel}
1463
+ thinkingLevel={heroThinking || undefined}
1464
+ thinkingLevels={heroModelLevels}
1465
+ models={models}
1466
+ modelLoading={modelLoading}
1467
+ modelLoadError={modelLoadError}
1468
+ onRetryModels={onRetryModels}
1469
+ providerNames={providerNames}
1470
+ authByProvider={authByProvider}
1471
+ queue={{ steering: [], followUp: [] }}
1472
+ workflow={{ mode: heroMode, planStatus: "idle", goal: "" }}
1473
+ onWorkflowModeChange={onSelectHeroMode}
1474
+ onSend={onSend}
1475
+ onSteer={() => ({ success: false })}
1476
+ onAbort={() => {}}
1477
+ onSelectModel={onSelectHeroModel}
1478
+ onSelectLevel={onSelectHeroThinking}
1479
+ />
1480
+ <div className="flex items-center justify-between px-2 pt-2 text-slate-500 dark:text-slate-400 font-mono text-xs font-medium">
1481
+ <div className="flex items-center gap-3">
1482
+ <span className="flex items-center gap-1"><kbd className="px-1.5 py-0.5 rounded clay-inset font-bold text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-slate-700">⏎</kbd> 发送</span>
1483
+ <span className="flex items-center gap-1"><kbd className="px-1.5 py-0.5 rounded clay-inset font-bold text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-slate-700">⇧ ⏎</kbd> 换行</span>
1484
+ </div>
1485
+ <div className="flex items-center gap-1.5 text-emerald-700 dark:text-emerald-400 font-semibold">
1486
+ <IconShieldOutline16 size={13} />
1487
+ <span>主会话无系统级沙盒隔离</span>
1488
+ </div>
1489
+ </div>
1490
+ </div>
1491
+
1492
+ {/* Telemetry bottom bar */}
1493
+ <div className="flex items-center justify-center gap-4 text-slate-600 dark:text-slate-300 text-xs font-semibold tracking-wider">
1494
+ <div className="flex items-center gap-1.5">
1495
+ <span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
1496
+ <span>极速热加载</span>
1497
+ </div>
1498
+ <span>•</span>
1499
+ <div className="flex items-center gap-1.5">
1500
+ <span className="w-1.5 h-1.5 rounded-full bg-sky-500" />
1501
+ <span>MCP 扩展全兼容</span>
1502
+ </div>
1503
+ <span>•</span>
1504
+ <div className="flex items-center gap-1.5">
1505
+ <span className="w-1.5 h-1.5 rounded-full bg-orange-500" />
1506
+ <span>端侧安全加固</span>
1507
+ </div>
1508
+ </div>
1203
1509
  </div>
1204
1510
  </div>
1205
1511
  );