@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
@@ -95,57 +95,80 @@ export function SettingsPanel({
95
95
  }}
96
96
  >
97
97
  <div
98
- className="settings-dialog glass-modal flex overflow-hidden"
98
+ className="settings-dialog clay-card relative flex flex-col gap-4 overflow-hidden bg-white/95 dark:bg-slate-900/95 p-6 shadow-2xl backdrop-blur-md"
99
99
  style={{
100
- width: 800,
101
- maxWidth: "calc(100vw - 48px)",
102
- height: "min(800px, calc(100vh - 48px))",
103
- borderRadius: 32,
104
- boxShadow: "var(--dsw-elevation-prominent)",
100
+ width: 960,
101
+ maxWidth: "calc(100vw - 32px)",
102
+ height: "min(840px, calc(100vh - 48px))",
103
+ borderRadius: 36,
105
104
  }}
106
105
  >
107
- {/* 左导航(dsh trigger:h42 r12,active 填充) */}
108
- <div className="settings-nav flex w-52 flex-none flex-col gap-0.5 py-4 pl-4 pr-3">
109
- <div className="settings-title px-2 pb-3" style={{ fontSize: 16, fontWeight: 500 }}>
110
- {t.settings}
106
+ {/* Top Header Bar (Screen 4-7) */}
107
+ <div className="flex items-center justify-between pb-3 border-b border-slate-200/60 dark:border-slate-800/60 shrink-0">
108
+ <div className="flex items-center gap-3">
109
+ <div className="w-10 h-10 rounded-2xl clay-inset flex items-center justify-center text-emerald-600 dark:text-emerald-400 flex-none">
110
+ <IconSettingsOutline16 size={20} />
111
+ </div>
112
+ <div className="flex flex-col">
113
+ <div className="flex items-center gap-2">
114
+ <h1 className="text-base sm:text-lg font-extrabold text-slate-800 dark:text-slate-100 tracking-tight">全局设置中心</h1>
115
+ <span className="px-2.5 py-0.5 rounded-full clay-inset text-emerald-600 dark:text-emerald-400 font-mono text-[11px] font-semibold">config.json</span>
116
+ </div>
117
+ <p className="text-xs text-slate-400 hidden sm:block">管理工作台外观基调、语言偏好、模型调度与集成插件环境</p>
118
+ </div>
111
119
  </div>
112
- {navItems.map((n) => (
120
+ <div className="flex items-center gap-2">
113
121
  <button
114
- key={n.id}
115
- className="flex h-[42px] items-center gap-2 rounded-xl px-3 text-left transition-colors"
116
- style={{
117
- paddingLeft: 8,
118
- paddingRight: 10,
119
- fontSize: 14,
120
- background: section === n.id ? "var(--dsw-active)" : "transparent",
121
- color: section === n.id ? "var(--dsw-label-primary)" : "var(--dsw-label-secondary)",
122
- }}
123
- onMouseEnter={(e) => {
124
- if (section !== n.id) e.currentTarget.style.background = "var(--dsw-hover)";
125
- }}
126
- onMouseLeave={(e) => {
127
- if (section !== n.id) e.currentTarget.style.background = "transparent";
128
- }}
129
- onClick={() => setSection(n.id)}
122
+ type="button"
123
+ className="clay-btn clay-btn-soft px-3 py-1.5 text-xs text-slate-700 dark:text-slate-200 flex items-center gap-1.5"
124
+ onClick={() => window.open("/api/config", "_blank")}
130
125
  >
131
- {section === n.id ? <span style={{ color: "var(--dsw-accent)" }}>{n.icon}</span> : n.icon}
132
- {n.label}
133
- </button>
134
- ))}
135
- </div>
136
-
137
- {/* 右侧内容 */}
138
- <div className="flex min-w-0 flex-1 flex-col">
139
- <div className="flex items-center gap-2 px-6 py-3.5">
140
- <div className="flex-1" />
141
- <button className="btn-outline" style={{ height: 32, fontSize: 13 }} onClick={() => window.open("/api/config", "_blank")}>
142
- <IconDownloadOutline16 size={14} /> {t.openConfigFile}
126
+ <IconDownloadOutline16 size={14} className="text-sky-500" />
127
+ <span>{t.openConfigFile}</span>
143
128
  </button>
144
- <button className="icon-btn" onClick={onClose}>
129
+ <button
130
+ type="button"
131
+ className="clay-btn clay-btn-soft w-8 h-8 rounded-full text-slate-500 hover:text-rose-500 transition-colors"
132
+ aria-label={t.close}
133
+ onClick={onClose}
134
+ >
145
135
  <IconCloseOutline14 size={15} />
146
136
  </button>
147
137
  </div>
148
- <div className="min-h-0 flex-1 overflow-y-auto px-6 py-5">
138
+ </div>
139
+
140
+ {/* 12-Column Grid Body */}
141
+ <div className="grid grid-cols-12 gap-5 min-h-0 flex-1 overflow-hidden">
142
+ {/* Left Sub-Navigation Menu (3 cols) */}
143
+ <div className="col-span-12 md:col-span-3 flex flex-col gap-3 min-h-0 overflow-y-auto">
144
+ <div className="clay-inset p-2 rounded-2xl flex flex-col gap-1.5 shrink-0">
145
+ {navItems.map((n) => {
146
+ const isSelected = section === n.id;
147
+ return (
148
+ <button
149
+ key={n.id}
150
+ type="button"
151
+ className={`flex items-center justify-between px-3.5 py-2.5 rounded-xl text-xs font-semibold transition-all ${
152
+ isSelected
153
+ ? "clay-card text-emerald-700 dark:text-emerald-300 font-bold bg-white dark:bg-slate-800 shadow-sm"
154
+ : "text-slate-600 dark:text-slate-300 hover:bg-white/60 dark:hover:bg-slate-800/60"
155
+ }`}
156
+ onClick={() => setSection(n.id)}
157
+ >
158
+ <div className="flex items-center gap-2.5">
159
+ <span className={isSelected ? "text-emerald-500" : "text-slate-400"}>{n.icon}</span>
160
+ <span>{n.label}</span>
161
+ </div>
162
+ {isSelected && <span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />}
163
+ </button>
164
+ );
165
+ })}
166
+ </div>
167
+
168
+ </div>
169
+
170
+ {/* Right Configuration Content Area (9 cols) */}
171
+ <div className="col-span-12 md:col-span-9 min-h-0 flex-1 overflow-y-auto pr-2">
149
172
  {section === "general" && <GeneralSection lang={lang} setLang={setLang} />}
150
173
  {section === "models" && <ModelsSection />}
151
174
  {section === "skills" && <SkillsSection cwd={cwd} onOpenFileContent={onOpenFileContent} />}
@@ -161,6 +184,16 @@ export function SettingsPanel({
161
184
 
162
185
  // ---------- General ----------
163
186
 
187
+ const THEME_OPTIONS: Array<{ id: PebrelTheme; labelKey: string; nameZh: string; nameEn: string; light: string; dark: string; accent: string }> = [
188
+ { id: "piweb", labelKey: "themePiWeb", nameZh: "PiWeb · 工作台", nameEn: "PiWeb · Workbench", light: "#34D399", dark: "#A7F3D0", accent: "#34D399" },
189
+ { id: "ocean-ink", labelKey: "themeOceanInk", nameZh: "海与墨", nameEn: "Ocean Ink", light: "#38BDF8", dark: "#0284C7", accent: "#38BDF8" },
190
+ { id: "silver-steel", labelKey: "themeSilverSteel", nameZh: "银钢", nameEn: "Silver Steel", light: "#E2E8F0", dark: "#475569", accent: "#94A3B8" },
191
+ { id: "limestone-coal", labelKey: "themeLimestoneCoal", nameZh: "石灰与煤炭", nameEn: "Limestone & Coal", light: "#FB923C", dark: "#1E293B", accent: "#FB923C" },
192
+ { id: "linen-moss", labelKey: "themeLinenMoss", nameZh: "亚麻与苔绿", nameEn: "Linen & Moss", light: "#FFDCC5", dark: "#006C4B", accent: "#006C4B" },
193
+ { id: "nord-paper", labelKey: "themeNordPaper", nameZh: "北欧纸墨", nameEn: "Nordic Paper", light: "#F6FAFE", dark: "#00668A", accent: "#00668A" },
194
+ { id: "pebrel", labelKey: "themePebrel", nameZh: "Pebrel", nameEn: "Pebrel", light: "#818CF8", dark: "#EEF2FF", accent: "#818CF8" },
195
+ ];
196
+
164
197
  function GeneralSection({
165
198
  lang,
166
199
  setLang,
@@ -169,7 +202,7 @@ function GeneralSection({
169
202
  setLang: (l: "zh" | "en") => void;
170
203
  }) {
171
204
  const { t } = useI18n();
172
- const [theme, setTheme] = useState<PebrelTheme>("dsh");
205
+ const [theme, setTheme] = useState<PebrelTheme>("piweb");
173
206
  const [themeMode, setThemeMode] = useState<ThemeMode>("system");
174
207
  const [fontSize, setFontSize] = useState(14);
175
208
  const [chatFontSize, setChatFontSize] = useState(14);
@@ -179,6 +212,8 @@ function GeneralSection({
179
212
  const [piSettings, setPiSettings] = useState<{ compaction: { enabled: boolean; reserveTokens: number; keepRecentTokens: number }; retry: { enabled: boolean; maxRetries: number; baseDelayMs: number } } | null>(null);
180
213
  const [versions, setVersions] = useState<{ piWeb: string; piEngine: string } | null>(null);
181
214
  const [webAuth, setWebAuth] = useState<{ enabled: boolean; authenticated: boolean } | null>(null);
215
+ const [logoutPending, setLogoutPending] = useState(false);
216
+ const [logoutError, setLogoutError] = useState(false);
182
217
  const [settingsLoading, setSettingsLoading] = useState(true);
183
218
  const [settingsSaving, setSettingsSaving] = useState(false);
184
219
  const [settingsError, setSettingsError] = useState<"load" | "save" | null>(null);
@@ -333,14 +368,17 @@ function GeneralSection({
333
368
  }
334
369
  };
335
370
 
336
- const themes: { id: PebrelTheme; label: string; light: string; dark: string; accent: string }[] = [
337
- { id: "dsh", label: t.themeDsh, light: "#F9FAFB", dark: "#151517", accent: "#4176E6" },
338
- { id: "silver-steel", label: t.themeSilverSteel, light: "#F3F4F6", dark: "#1A1C24", accent: "#94A3B8" },
339
- { id: "limestone-coal", label: t.themeLimestoneCoal, light: "#F0EFEB", dark: "#171717", accent: "#CEB27E" },
340
- { id: "linen-moss", label: t.themeLinenMoss, light: "#F2F2EC", dark: "#1E211E", accent: "#A3B3A3" },
341
- { id: "nord-paper", label: t.themeNordPaper, light: "#FCFBF9", dark: "#2E3440", accent: "#88C0D0" },
342
- { id: "pebrel", label: t.themePebrel, light: "#F3F4F6", dark: "#0F111A", accent: "#52A8FF" },
343
- ];
371
+ const themes = useMemo(
372
+ () =>
373
+ THEME_OPTIONS.map((th) => ({
374
+ id: th.id,
375
+ label: (t as Record<string, string>)[th.labelKey] || (lang === "zh" ? th.nameZh : th.nameEn),
376
+ light: th.light,
377
+ dark: th.dark,
378
+ accent: th.accent,
379
+ })),
380
+ [lang, t],
381
+ );
344
382
 
345
383
  return (
346
384
  <div className="flex flex-col gap-6">
@@ -361,38 +399,34 @@ function GeneralSection({
361
399
  />
362
400
  </Row>
363
401
  <RowColumn title={t.appearance}>
364
- <div className="flex w-full flex-wrap gap-2">
365
- {themes.map((th) => (
366
- <button
367
- key={th.id}
368
- className="flex flex-col items-center justify-center gap-1 transition-colors"
369
- style={{
370
- flex: "1 1 150px",
371
- padding: "20px 32px",
372
- borderRadius: 20,
373
- border: `0.5px solid ${theme === th.id ? "var(--dsw-border-strong)" : "var(--dsw-border-l4)"}`,
374
- background: theme === th.id ? "var(--dsw-module-platform)" : "transparent",
375
- color: "var(--dsw-label-primary)",
376
- fontSize: 14,
377
- lineHeight: "22px",
378
- }}
379
- aria-pressed={theme === th.id}
380
- onClick={() => pickTheme(th.id)}
381
- >
382
- <span className="flex items-center overflow-hidden rounded-md" style={{ border: "0.5px solid var(--dsw-border-l3)", width: 42, height: 20 }}>
383
- <span style={{ background: th.light, flex: 1, height: "100%" }} />
384
- <span style={{ background: th.dark, flex: 1, height: "100%" }} />
385
- </span>
386
- <span className="flex items-center gap-1.5">
387
- <span className="inline-block rounded-full" style={{ width: 8, height: 8, background: th.accent }} />
388
- {th.label}
389
- </span>
390
- </button>
391
- ))}
402
+ <div className="settings-theme-grid grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2.5 w-full">
403
+ {themes.map((th) => {
404
+ const isSelected = theme === th.id;
405
+ return (
406
+ <button
407
+ key={th.id}
408
+ type="button"
409
+ className={`clay-card flex flex-col items-center justify-center p-3.5 rounded-2xl transition-all hover:-translate-y-0.5 ${
410
+ isSelected ? "ring-2 ring-emerald-500 shadow-md bg-white dark:bg-slate-800" : "bg-white/70 dark:bg-slate-900/60 opacity-80 hover:opacity-100"
411
+ }`}
412
+ aria-pressed={isSelected}
413
+ onClick={() => pickTheme(th.id)}
414
+ >
415
+ <div className="flex items-center overflow-hidden rounded-lg mb-2 shadow-inner border border-slate-200/50 dark:border-slate-700/50" style={{ width: 48, height: 22 }}>
416
+ <span style={{ background: th.light, flex: 1, height: "100%" }} />
417
+ <span style={{ background: th.dark, flex: 1, height: "100%" }} />
418
+ </div>
419
+ <div className="flex items-center gap-1.5 text-xs font-medium text-slate-800 dark:text-slate-100">
420
+ <span className="w-2 h-2 rounded-full inline-block flex-none" style={{ background: th.accent }} />
421
+ <span className="truncate">{th.label}</span>
422
+ </div>
423
+ </button>
424
+ );
425
+ })}
392
426
  </div>
393
427
  </RowColumn>
394
428
  <RowColumn title={t.appearanceMode}>
395
- <div className="flex w-full flex-wrap gap-2">
429
+ <div className="clay-inset flex p-1 rounded-2xl bg-slate-100/90 dark:bg-slate-900/80 gap-1 w-full max-w-sm">
396
430
  {([
397
431
  { id: "light", label: t.themeLight },
398
432
  { id: "dark", label: t.themeDark },
@@ -400,17 +434,12 @@ function GeneralSection({
400
434
  ] as { id: ThemeMode; label: string }[]).map((mode) => (
401
435
  <button
402
436
  key={mode.id}
403
- className="flex flex-col items-center justify-center gap-1 transition-colors"
404
- style={{
405
- flex: "1 1 120px",
406
- padding: "16px 24px",
407
- borderRadius: 20,
408
- border: `0.5px solid ${themeMode === mode.id ? "var(--dsw-border-strong)" : "var(--dsw-border-l4)"}`,
409
- background: themeMode === mode.id ? "var(--dsw-module-platform)" : "transparent",
410
- color: "var(--dsw-label-primary)",
411
- fontSize: 14,
412
- lineHeight: "22px",
413
- }}
437
+ type="button"
438
+ className={`flex-1 py-1.5 px-3 rounded-xl text-xs font-semibold transition-all ${
439
+ themeMode === mode.id
440
+ ? "clay-btn-soft text-slate-800 dark:text-slate-100 shadow-sm"
441
+ : "text-slate-500 hover:text-slate-800 dark:hover:text-slate-200"
442
+ }`}
414
443
  aria-pressed={themeMode === mode.id}
415
444
  onClick={() => pickThemeMode(mode.id)}
416
445
  >
@@ -437,7 +466,7 @@ function GeneralSection({
437
466
  </Row>
438
467
  <Row title={t.autoCompact} desc={t.autoCompactDesc}>
439
468
  <div className="flex items-center gap-2">
440
- <button disabled={!piSettings || settingsSaving} className="relative h-5 w-9 flex-none rounded-full transition-colors" style={{ background: piSettings?.compaction.enabled ? "var(--dsw-accent)" : "var(--dsw-border-l3)" }} role="switch" aria-checked={piSettings?.compaction.enabled ?? false} onClick={() => void patchPiSettings({ compaction: { enabled: !piSettings?.compaction.enabled } })}>
469
+ <button disabled={!piSettings || settingsSaving} className="relative h-5 w-9 flex-none rounded-full transition-colors" style={{ background: piSettings?.compaction.enabled ? "var(--dsw-accent)" : "var(--dsw-border-l3)" }} role="switch" aria-label={t.autoCompact} aria-checked={piSettings?.compaction.enabled ?? false} onClick={() => void patchPiSettings({ compaction: { enabled: !piSettings?.compaction.enabled } })}>
441
470
  <span className="absolute top-0.5 h-4 w-4 rounded-full bg-white transition-all" style={{ left: piSettings?.compaction.enabled ? 18 : 2 }} />
442
471
  </button>
443
472
  {numberInput("compaction", "reserveTokens", t.reserveTokens)}
@@ -446,7 +475,7 @@ function GeneralSection({
446
475
  </Row>
447
476
  <Row title={t.autoRetry} desc={t.autoRetryDesc}>
448
477
  <div className="flex items-center gap-2">
449
- <button disabled={!piSettings || settingsSaving} className="relative h-5 w-9 flex-none rounded-full transition-colors" style={{ background: piSettings?.retry.enabled ? "var(--dsw-accent)" : "var(--dsw-border-l3)" }} role="switch" aria-checked={piSettings?.retry.enabled ?? false} onClick={() => void patchPiSettings({ retry: { enabled: !piSettings?.retry.enabled } })}>
478
+ <button disabled={!piSettings || settingsSaving} className="relative h-5 w-9 flex-none rounded-full transition-colors" style={{ background: piSettings?.retry.enabled ? "var(--dsw-accent)" : "var(--dsw-border-l3)" }} role="switch" aria-label={t.autoRetry} aria-checked={piSettings?.retry.enabled ?? false} onClick={() => void patchPiSettings({ retry: { enabled: !piSettings?.retry.enabled } })}>
450
479
  <span className="absolute top-0.5 h-4 w-4 rounded-full bg-white transition-all" style={{ left: piSettings?.retry.enabled ? 18 : 2 }} />
451
480
  </button>
452
481
  {numberInput("retry", "maxRetries", t.maxRetries)}
@@ -479,12 +508,24 @@ function GeneralSection({
479
508
  </Row>
480
509
  {webAuth?.enabled ? (
481
510
  <Row title={t.webAuth} desc={t.webAuthOn}>
511
+ {logoutError && <span role="alert" style={{ color: "var(--dsw-danger)", fontSize: 12 }}>{t.logoutFailed}</span>}
482
512
  <button
483
513
  className="btn-outline"
484
514
  style={{ height: 30, padding: "0 14px", fontSize: 12.5 }}
515
+ disabled={logoutPending}
485
516
  onClick={async () => {
486
- await fetch("/api/web-auth", { method: "DELETE" }).catch(() => {});
487
- window.location.href = "/login";
517
+ if (logoutPending) return;
518
+ setLogoutPending(true);
519
+ setLogoutError(false);
520
+ try {
521
+ const response = await fetch("/api/web-auth", { method: "DELETE" });
522
+ const result = await response.json();
523
+ if (!response.ok || !result.success) throw new Error("logout failed");
524
+ window.location.href = "/login";
525
+ } catch {
526
+ setLogoutError(true);
527
+ setLogoutPending(false);
528
+ }
488
529
  }}
489
530
  >
490
531
  {t.logout}
@@ -695,7 +736,7 @@ function SelectOption({
695
736
 
696
737
  function Row({ title, desc, children }: { title: string; desc?: string; children: React.ReactNode }) {
697
738
  return (
698
- <div className="hairline-b flex items-center justify-between gap-6 pb-5">
739
+ <div className="settings-row hairline-b flex items-center justify-between gap-6 pb-5">
699
740
  <div className="min-w-0">
700
741
  <div style={{ fontSize: 14, fontWeight: 400 }}>{title}</div>
701
742
  {desc && (
@@ -704,7 +745,7 @@ function Row({ title, desc, children }: { title: string; desc?: string; children
704
745
  </div>
705
746
  )}
706
747
  </div>
707
- <div className="flex-none">{children}</div>
748
+ <div className="settings-row-control flex-none">{children}</div>
708
749
  </div>
709
750
  );
710
751
  }
@@ -729,10 +770,10 @@ function RowColumn({ title, desc, children }: { title: string; desc?: string; ch
729
770
  /** 字号步进器:− 值 +(越界禁用;点中间数值回到 14 默认) */
730
771
  function FontSizeStepper({ value, onChange, max }: { value: number; onChange: (v: number) => void; max: number }) {
731
772
  return (
732
- <div className="flex items-center gap-2">
773
+ <div className="clay-inset flex items-center gap-1.5 p-1 rounded-2xl bg-slate-100 dark:bg-slate-900">
733
774
  <button
734
- className="icon-btn"
735
- style={{ width: 26, height: 26, border: "0.5px solid var(--dsw-border-l3)", borderRadius: 8 }}
775
+ type="button"
776
+ className="clay-btn clay-btn-soft w-7 h-7 rounded-xl text-xs font-bold text-slate-700 dark:text-slate-200 disabled:opacity-40"
736
777
  disabled={value <= 12}
737
778
  onClick={() => onChange(value - 1)}
738
779
  aria-label="decrease font size"
@@ -740,15 +781,16 @@ function FontSizeStepper({ value, onChange, max }: { value: number; onChange: (v
740
781
  −
741
782
  </button>
742
783
  <button
743
- style={{ minWidth: 48, textAlign: "center", fontSize: 13, color: value === 14 ? "var(--dsw-label-tertiary)" : "var(--dsw-label-primary)" }}
744
- title="reset"
784
+ type="button"
785
+ className="px-2 text-center text-xs font-semibold text-slate-700 dark:text-slate-200 hover:text-emerald-500 transition-colors"
786
+ title="reset to 14px"
745
787
  onClick={() => onChange(14)}
746
788
  >
747
789
  {value} px
748
790
  </button>
749
791
  <button
750
- className="icon-btn"
751
- style={{ width: 26, height: 26, border: "0.5px solid var(--dsw-border-l3)", borderRadius: 8 }}
792
+ type="button"
793
+ className="clay-btn clay-btn-soft w-7 h-7 rounded-xl text-xs font-bold text-slate-700 dark:text-slate-200 disabled:opacity-40"
752
794
  disabled={value >= max}
753
795
  onClick={() => onChange(value + 1)}
754
796
  aria-label="increase font size"
@@ -1203,13 +1245,19 @@ function ModelsSection() {
1203
1245
 
1204
1246
  return (
1205
1247
  <div className="flex flex-col gap-5">
1206
- <div>
1207
- <div style={{ fontSize: 14, fontWeight: 400 }}>{t.modelsTitle}</div>
1208
- <div className="mt-0.5" style={{ fontSize: 12.5, color: "var(--dsw-label-caption)" }}>
1209
- {t.modelsDesc}
1248
+ {/* Notice Banner (Screen 6 Soft Claymorphism) */}
1249
+ <div className="p-4 rounded-2xl bg-emerald-50/80 dark:bg-emerald-950/30 border border-emerald-200/50 dark:border-emerald-800/40 flex items-start gap-3">
1250
+ <div className="w-8 h-8 rounded-full bg-white dark:bg-slate-800 text-emerald-600 clay-card flex-shrink-0 flex items-center justify-center mt-0.5 shadow-sm">
1251
+ <IconModelOutline16 size={16} />
1210
1252
  </div>
1211
- <div className="mt-1" style={{ fontSize: 12, color: "var(--dsw-label-caption)" }}>
1212
- {t.customModelIdentityNotice}
1253
+ <div className="flex flex-col gap-1 min-w-0">
1254
+ <span className="font-bold text-slate-800 dark:text-slate-100 text-sm">{t.modelsTitle}</span>
1255
+ <p className="text-xs text-slate-600 dark:text-slate-300 leading-relaxed">
1256
+ {t.modelsDesc}
1257
+ </p>
1258
+ <p className="text-[11px] text-slate-400 dark:text-slate-400">
1259
+ {t.customModelIdentityNotice}
1260
+ </p>
1213
1261
  </div>
1214
1262
  </div>
1215
1263
 
@@ -1233,8 +1281,8 @@ function ModelsSection() {
1233
1281
  const editingThis = editing === p.id;
1234
1282
  return (
1235
1283
  <div key={p.id}>
1236
- {/* 卡片:身份/操作行在上,额度显示放在同一个框内的第二段(分隔线区分) */}
1237
- <div className="flex flex-col rounded-2xl px-4 py-3" style={{ border: "0.5px solid var(--dsw-border-l2)" }}>
1284
+ {/* 卡片:身份/操作行在上,额度显示放在同一个框内的第二段(柔和粘土拟物风格) */}
1285
+ <div className="clay-card flex flex-col rounded-2xl px-4 py-3 bg-white/80 dark:bg-slate-900/80 shadow-sm transition-all hover:shadow-md" style={{ border: "0.5px solid var(--dsw-border-l2)" }}>
1238
1286
  <div className="flex items-center gap-3">
1239
1287
  <ProviderBrand id={p.id} name={providerName(p.id)} size={34} />
1240
1288
  <div className="min-w-0 flex-1">
@@ -1452,30 +1500,39 @@ function ModelsSection() {
1452
1500
  })}
1453
1501
  </div>
1454
1502
 
1455
- {/* 两个添加按钮 */}
1456
- <div className="flex gap-3">
1503
+ {/* 两个添加按钮: Large Tactile Extruded Clay Buttons (Screen 6) */}
1504
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-1">
1457
1505
  <button
1458
- className="flex flex-1 items-center justify-center gap-2 rounded-2xl px-4 py-3.5 transition-colors"
1459
- style={{ border: "0.5px solid var(--dsw-border-l3)", fontSize: 14 }}
1460
- onMouseEnter={(e) => (e.currentTarget.style.background = "var(--dsw-hover)")}
1461
- onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
1506
+ type="button"
1507
+ className="py-3.5 px-5 rounded-2xl clay-card text-slate-800 dark:text-slate-100 font-bold flex items-center justify-center gap-2 hover:-translate-y-0.5 active:translate-y-0.5 transition-all cursor-pointer group shadow-sm hover:shadow"
1462
1508
  onClick={() => setAdding("builtin")}
1463
1509
  >
1464
- <IconPlusOutline16 size={15} />
1465
- {t.addProvider}
1510
+ <div className="w-7 h-7 rounded-full clay-inset flex items-center justify-center text-emerald-600 group-hover:scale-110 transition-transform">
1511
+ <IconPlusOutline16 size={15} />
1512
+ </div>
1513
+ <span>{t.addProvider}</span>
1466
1514
  </button>
1467
1515
  <button
1468
- className="flex flex-1 items-center justify-center gap-2 rounded-2xl px-4 py-3.5 transition-colors"
1469
- style={{ border: "0.5px solid var(--dsw-border-l3)", fontSize: 14 }}
1470
- onMouseEnter={(e) => (e.currentTarget.style.background = "var(--dsw-hover)")}
1471
- onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
1516
+ type="button"
1517
+ className="py-3.5 px-5 rounded-2xl clay-card text-slate-800 dark:text-slate-100 font-bold flex items-center justify-center gap-2 hover:-translate-y-0.5 active:translate-y-0.5 transition-all cursor-pointer group shadow-sm hover:shadow"
1472
1518
  onClick={() => setAdding("custom")}
1473
1519
  >
1474
- <IconPlusOutline16 size={15} />
1475
- {t.addCustomProvider}
1520
+ <div className="w-7 h-7 rounded-full clay-inset flex items-center justify-center text-orange-600 group-hover:scale-110 transition-transform">
1521
+ <IconPlusOutline16 size={15} />
1522
+ </div>
1523
+ <span>{t.addCustomProvider}</span>
1476
1524
  </button>
1477
1525
  </div>
1478
1526
 
1527
+ {/* 路由状态提示 */}
1528
+ <div className="clay-card p-4 rounded-2xl flex items-center gap-3 bg-gradient-to-r from-emerald-50/50 to-sky-50/50 dark:from-emerald-950/20 dark:to-sky-950/20 text-xs text-slate-600 dark:text-slate-300 border border-emerald-100/60 dark:border-emerald-950/40">
1529
+ <span className="text-emerald-500 font-bold flex items-center gap-1.5 flex-none">
1530
+ <span className="w-2 h-2 rounded-full bg-emerald-500 inline-block" />
1531
+ 路由提示:
1532
+ </span>
1533
+ <span>请求使用当前选定的模型与供应商;此处未配置自动故障转移。</span>
1534
+ </div>
1535
+
1479
1536
  {adding ? (
1480
1537
  <ProviderSetupModal
1481
1538
  mode={adding}
@@ -1686,59 +1743,115 @@ function ToolsSection({
1686
1743
  return (
1687
1744
  <div className="flex flex-col gap-5">
1688
1745
  <div>
1689
- <div style={{ fontSize: 14, fontWeight: 400 }}>{t.toolsSection}</div>
1746
+ <div style={{ fontSize: 16, fontWeight: 600, color: "var(--dsw-label-primary)" }}>{t.toolsSection}</div>
1690
1747
  <div className="mt-0.5" style={{ fontSize: 12.5, color: "var(--dsw-label-caption)" }}>
1691
- {t.toolsSectionDesc}
1748
+ 当前会话生效的工具与权限预设;主会话不提供系统级沙盒隔离
1692
1749
  </div>
1693
1750
  </div>
1694
1751
 
1695
- <Row title={t.securityToolPreset} desc={t.securityToolPresetDesc}>
1696
- <SelectOption
1697
- value={toolPreset}
1698
- options={[
1699
- { value: "readonly", label: t.toolReadonly },
1700
- { value: "standard", label: t.toolStandard },
1701
- { value: "full", label: t.toolFull },
1702
- ]}
1703
- onChange={(value) => onToolPresetChange(value as ToolPreset)}
1704
- />
1705
- </Row>
1752
+ {/* 3-tier permission modes (Screen 5) */}
1753
+ <div className="flex flex-col gap-2">
1754
+ <div className="text-xs font-medium text-slate-500">运行权限预设</div>
1755
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
1756
+ <button
1757
+ type="button"
1758
+ className={`clay-card p-3 rounded-2xl text-left transition-all ${
1759
+ toolPreset === "readonly" ? "ring-2 ring-emerald-500 bg-white dark:bg-slate-800 shadow-md" : "bg-white/70 dark:bg-slate-900/60 opacity-85 hover:opacity-100"
1760
+ }`}
1761
+ onClick={() => onToolPresetChange("readonly")}
1762
+ >
1763
+ <div className="font-semibold text-xs text-slate-800 dark:text-slate-100 flex items-center justify-between">
1764
+ <span>{t.toolReadonly}</span>
1765
+ {toolPreset === "readonly" && <IconCheckOutline14 className="text-emerald-500" size={13} />}
1766
+ </div>
1767
+ <div className="text-[11px] text-slate-500 mt-1 leading-relaxed">仅读取文件与搜索,禁止修改系统或执行命令</div>
1768
+ </button>
1769
+
1770
+ <button
1771
+ type="button"
1772
+ className={`clay-card p-3 rounded-2xl text-left transition-all ${
1773
+ toolPreset === "standard" ? "ring-2 ring-emerald-500 bg-white dark:bg-slate-800 shadow-md" : "bg-white/70 dark:bg-slate-900/60 opacity-85 hover:opacity-100"
1774
+ }`}
1775
+ onClick={() => onToolPresetChange("standard")}
1776
+ >
1777
+ <div className="font-semibold text-xs text-slate-800 dark:text-slate-100 flex items-center justify-between">
1778
+ <span>{t.toolStandard}</span>
1779
+ {toolPreset === "standard" && <IconCheckOutline14 className="text-emerald-500" size={13} />}
1780
+ </div>
1781
+ <div className="text-[11px] text-slate-500 mt-1 leading-relaxed">常规开发工具集,终端执行与高危修改需确认</div>
1782
+ </button>
1783
+
1784
+ <button
1785
+ type="button"
1786
+ className={`clay-card p-3 rounded-2xl text-left transition-all ${
1787
+ toolPreset === "full" ? "ring-2 ring-emerald-500 bg-white dark:bg-slate-800 shadow-md" : "bg-white/70 dark:bg-slate-900/60 opacity-85 hover:opacity-100"
1788
+ }`}
1789
+ onClick={() => onToolPresetChange("full")}
1790
+ >
1791
+ <div className="font-semibold text-xs text-slate-800 dark:text-slate-100 flex items-center justify-between">
1792
+ <span>{t.toolFull}</span>
1793
+ {toolPreset === "full" && <IconCheckOutline14 className="text-emerald-500" size={13} />}
1794
+ </div>
1795
+ <div className="text-[11px] text-slate-500 mt-1 leading-relaxed">允许所有内置与扩展工具无障碍全自动调用</div>
1796
+ </button>
1797
+ </div>
1798
+ </div>
1799
+
1800
+ {/* Telemetry card / Context caching widget */}
1801
+ <div className="clay-card p-3 rounded-2xl bg-emerald-50/40 dark:bg-emerald-950/20 border border-emerald-200/50 dark:border-emerald-900/40 flex items-center justify-between text-xs">
1802
+ <div className="flex items-center gap-2">
1803
+ <span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
1804
+ <span className="font-medium text-slate-700 dark:text-slate-200">会话工具:</span>
1805
+ <span className="text-emerald-600 dark:text-emerald-400 font-semibold">{active.size}/{all.length} 已启用</span>
1806
+ </div>
1807
+ <div className="text-slate-400 text-[11px] font-mono">
1808
+ 按当前会话工具清单统计
1809
+ </div>
1810
+ </div>
1706
1811
 
1707
1812
  {all.length === 0 ? (
1708
- <div className="rounded-2xl p-4" style={{ border: "0.5px dashed var(--dsw-border-l3)", fontSize: 12.5, color: "var(--dsw-label-caption)" }}>
1813
+ <div className="clay-card rounded-2xl p-6 text-center text-xs text-slate-400">
1709
1814
  {t.toolsNoSession}
1710
1815
  </div>
1711
1816
  ) : (
1712
- <div className="flex flex-col gap-2.5">
1713
- <div className="flex items-center gap-2" style={{ fontSize: 12, color: "var(--dsw-label-caption)" }}>
1714
- <span style={{ fontWeight: 600 }}>{t.toolsCurrentSession}</span>
1817
+ <div className="flex flex-col gap-2">
1818
+ <div className="flex items-center justify-between text-xs text-slate-500 px-1">
1819
+ <span className="font-medium">{t.toolsCurrentSession}</span>
1715
1820
  <span>{t.toolsActiveCount.replace("{n}", String(active.size)).replace("{total}", String(all.length))}</span>
1716
1821
  </div>
1717
1822
  {all.map((tool) => {
1718
1823
  const on = active.has(tool.name);
1719
1824
  const desc = tool.description || describe(tool.name);
1720
1825
  return (
1721
- <div key={tool.name} className="flex items-center gap-3 rounded-2xl px-4 py-2.5" style={{ border: "0.5px solid var(--dsw-border-l2)" }}>
1722
- <div className="min-w-0 flex-1">
1723
- <div style={{ fontSize: 13.5, fontWeight: 500, fontFamily: "var(--font-mono)" }}>{tool.name}</div>
1826
+ <div key={tool.name} className="clay-card flex items-center justify-between p-3 rounded-2xl transition-all hover:translate-x-0.5">
1827
+ <div className="min-w-0 flex-1 pr-3">
1828
+ <div className="flex items-center gap-2">
1829
+ <span className="clay-code-pill px-2.5 py-0.5 text-xs font-mono font-bold text-slate-800 dark:text-slate-200">
1830
+ {tool.name}
1831
+ </span>
1832
+ </div>
1724
1833
  {desc && (
1725
- <div className="truncate" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
1834
+ <div className="truncate text-xs text-slate-500 mt-1">
1726
1835
  {desc}
1727
1836
  </div>
1728
1837
  )}
1729
1838
  </div>
1730
1839
  <button
1731
- className="relative h-5 w-9 flex-none rounded-full transition-colors"
1732
- style={{ background: on ? "var(--dsw-accent)" : "var(--dsw-border-l3)" }}
1840
+ type="button"
1841
+ className={`clay-btn w-12 h-6 rounded-full transition-all duration-200 p-0.5 flex items-center ${
1842
+ on ? "clay-btn-mint justify-end" : "bg-slate-200 dark:bg-slate-700 text-slate-400 justify-start"
1843
+ }`}
1733
1844
  role="switch"
1734
1845
  aria-checked={on}
1735
1846
  aria-label={tool.name}
1736
1847
  disabled={!onSetTools || (on && active.size <= 1)}
1737
1848
  onClick={() => toggle(tool.name)}
1738
1849
  >
1739
- <span className="absolute top-0.5 h-4 w-4 rounded-full bg-white transition-all" style={{ left: on ? 18 : 2 }} />
1850
+ <span className="w-5 h-5 rounded-full bg-white shadow-md flex items-center justify-center text-[10px] font-bold text-emerald-600">
1851
+ {on ? "✓" : "−"}
1852
+ </span>
1740
1853
  </button>
1741
- </div>
1854
+ </div>
1742
1855
  );
1743
1856
  })}
1744
1857
  </div>
@@ -1760,49 +1873,54 @@ interface SkillRow {
1760
1873
  function SkillsSection({ cwd, onOpenFileContent }: { cwd: string; onOpenFileContent?: (path: string, content: string) => void }) {
1761
1874
  const { t } = useI18n();
1762
1875
  const [skills, setSkills] = useState<SkillRow[]>([]);
1876
+ const [error, setError] = useState<string | null>(null);
1877
+ const [loading, setLoading] = useState(true);
1878
+ const [busyFile, setBusyFile] = useState<string | null>(null);
1763
1879
 
1764
1880
  const load = useCallback(async () => {
1765
- const r = await fetch(`/api/skills${cwd ? `?cwd=${encodeURIComponent(cwd)}` : ""}`);
1766
- const j = await r.json();
1767
- if (j.success) setSkills(j.data.skills);
1768
- }, [cwd]);
1881
+ setLoading(true);
1882
+ try {
1883
+ const r = await fetch(`/api/skills${cwd ? `?cwd=${encodeURIComponent(cwd)}` : ""}`);
1884
+ const j = await r.json();
1885
+ if (!r.ok || !j.success) throw new Error(j.error || t.toastError);
1886
+ setSkills(j.data.skills);
1887
+ setError(null);
1888
+ } catch (cause) {
1889
+ setError(cause instanceof Error ? cause.message : t.toastError);
1890
+ } finally {
1891
+ setLoading(false);
1892
+ }
1893
+ }, [cwd, t.toastError]);
1769
1894
 
1770
1895
  useEffect(() => {
1771
1896
  void load();
1772
1897
  }, [load]);
1773
1898
 
1774
- const toggle = async (s: SkillRow) => {
1775
- const r = await fetch("/api/skills", {
1776
- method: "POST",
1777
- headers: { "Content-Type": "application/json" },
1778
- body: JSON.stringify({ action: "toggle", filePath: s.filePath, disabled: !s.disabled, cwd }),
1779
- });
1780
- if ((await r.json()).success) void load();
1899
+ const action = async (s: SkillRow, kind: "toggle" | "read" | "delete") => {
1900
+ if (busyFile) return;
1901
+ setBusyFile(s.filePath);
1902
+ setError(null);
1903
+ try {
1904
+ const r = await fetch("/api/skills", {
1905
+ method: "POST",
1906
+ headers: { "Content-Type": "application/json" },
1907
+ body: JSON.stringify({ action: kind, filePath: s.filePath, ...(kind === "toggle" ? { disabled: !s.disabled } : {}), cwd }),
1908
+ });
1909
+ const j = await r.json();
1910
+ if (!r.ok || !j.success) throw new Error(j.error || t.toastError);
1911
+ if (kind === "read") onOpenFileContent?.(s.filePath, j.data.content);
1912
+ else await load();
1913
+ } catch (cause) {
1914
+ setError(cause instanceof Error ? cause.message : t.toastError);
1915
+ } finally {
1916
+ setBusyFile(null);
1917
+ }
1781
1918
  };
1782
1919
 
1783
1920
  // 查看技能文档:弹大窗口查看器(与项目文件查看器同款,Markdown 可切渲染/源码)
1784
- const view = async (s: SkillRow) => {
1785
- const r = await fetch("/api/skills", {
1786
- method: "POST",
1787
- headers: { "Content-Type": "application/json" },
1788
- body: JSON.stringify({ action: "read", filePath: s.filePath, cwd }),
1789
- });
1790
- const j = await r.json();
1791
- if (j.success) onOpenFileContent?.(s.filePath, j.data.content);
1792
- };
1793
-
1794
- const [deleteError, setDeleteError] = useState<string | null>(null);
1795
1921
  const remove = async (s: SkillRow) => {
1796
1922
  if (!window.confirm(t.confirmDeleteSkill.replace("{name}", s.name))) return;
1797
- setDeleteError(null);
1798
- const r = await fetch("/api/skills", {
1799
- method: "POST",
1800
- headers: { "Content-Type": "application/json" },
1801
- body: JSON.stringify({ action: "delete", filePath: s.filePath, cwd }),
1802
- });
1803
- const j = await r.json();
1804
- if (j.success) void load();
1805
- else setDeleteError(j.error || t.toastError);
1923
+ await action(s, "delete");
1806
1924
  };
1807
1925
 
1808
1926
  const scopeLabel = (s: SkillRow["scope"]) => (s === "global" ? t.scopeGlobal : s === "project" ? t.scopeProject : t.scopePackage);
@@ -1810,13 +1928,15 @@ function SkillsSection({ cwd, onOpenFileContent }: { cwd: string; onOpenFileCont
1810
1928
  return (
1811
1929
  <div className="flex flex-col gap-4">
1812
1930
  <div style={{ fontSize: 12, color: "var(--dsw-label-caption)" }}>{t.skillsDesc}</div>
1813
- {deleteError && (
1931
+ {error && (
1814
1932
  <div role="alert" className="rounded-lg px-3 py-2" style={{ fontSize: 12.5, background: "var(--dsw-hover)", color: "var(--dsw-danger)" }}>
1815
- {deleteError}
1933
+ {error} <button type="button" className="underline" onClick={() => void load()}>{t.settingsRetry}</button>
1816
1934
  </div>
1817
1935
  )}
1818
- {skills.length === 0 && (
1819
- <div style={{ fontSize: 13, color: "var(--dsw-label-caption)" }}>—</div>
1936
+ {!error && skills.length === 0 && (
1937
+ <div role="status" style={{ fontSize: 13, color: "var(--dsw-label-caption)" }}>
1938
+ {loading ? t.skillsLoading : t.skillsEmpty}
1939
+ </div>
1820
1940
  )}
1821
1941
  <div className="flex flex-col gap-2">
1822
1942
  {skills.map((s) => (
@@ -1839,7 +1959,8 @@ function SkillsSection({ cwd, onOpenFileContent }: { cwd: string; onOpenFileCont
1839
1959
  <button
1840
1960
  className="rounded-lg px-2.5 py-1"
1841
1961
  style={{ fontSize: 11.5, color: "var(--dsw-label-tertiary)" }}
1842
- onClick={() => view(s)}
1962
+ disabled={busyFile !== null}
1963
+ onClick={() => void action(s, "read")}
1843
1964
  >
1844
1965
  {t.viewSkillFile}
1845
1966
  </button>
@@ -1848,6 +1969,7 @@ function SkillsSection({ cwd, onOpenFileContent }: { cwd: string; onOpenFileCont
1848
1969
  className="rounded-lg px-2.5 py-1"
1849
1970
  style={{ fontSize: 11.5, color: "var(--dsw-danger)" }}
1850
1971
  title={t.deleteSkillHint}
1972
+ disabled={busyFile !== null}
1851
1973
  onClick={() => void remove(s)}
1852
1974
  >
1853
1975
  {t.delete}
@@ -1862,8 +1984,10 @@ function SkillsSection({ cwd, onOpenFileContent }: { cwd: string; onOpenFileCont
1862
1984
  className="relative h-5 w-9 flex-none rounded-full transition-colors"
1863
1985
  style={{ background: s.disabled ? "var(--dsw-border-l3)" : "var(--dsw-accent)" }}
1864
1986
  role="switch"
1987
+ aria-label={s.name}
1865
1988
  aria-checked={!s.disabled}
1866
- onClick={() => toggle(s)}
1989
+ disabled={busyFile !== null}
1990
+ onClick={() => void action(s, "toggle")}
1867
1991
  >
1868
1992
  <span
1869
1993
  className="absolute top-0.5 h-4 w-4 rounded-full bg-white transition-all"
@@ -1893,6 +2017,7 @@ function PluginsSection({ cwd }: { cwd: string }) {
1893
2017
  const [busy, setBusy] = useState(false);
1894
2018
  const [busyKey, setBusyKey] = useState<string | null>(null);
1895
2019
  const [toast, setToast] = useState<{ ok: boolean; msg: string; loading?: boolean } | null>(null);
2020
+ const [loadError, setLoadError] = useState<string | null>(null);
1896
2021
  const [menuKey, setMenuKey] = useState<string | null>(null);
1897
2022
  const menuRef = useRef<HTMLDivElement | null>(null);
1898
2023
 
@@ -1906,13 +2031,17 @@ function PluginsSection({ cwd }: { cwd: string }) {
1906
2031
  }, []);
1907
2032
 
1908
2033
  const load = useCallback(async () => {
1909
- const r = await fetch(`/api/plugins${cwd ? `?cwd=${encodeURIComponent(cwd)}` : ""}`);
1910
- const j = await r.json();
1911
- if (j.success) {
2034
+ try {
2035
+ const r = await fetch(`/api/plugins${cwd ? `?cwd=${encodeURIComponent(cwd)}` : ""}`);
2036
+ const j = await r.json();
2037
+ if (!r.ok || !j.success) throw new Error(j.error || t.toastError);
1912
2038
  setPackages(j.data.packages);
1913
2039
  setExtensions(j.data.extensions);
2040
+ setLoadError(null);
2041
+ } catch (error) {
2042
+ setLoadError(error instanceof Error ? error.message : t.toastError);
1914
2043
  }
1915
- }, [cwd]);
2044
+ }, [cwd, t.toastError]);
1916
2045
 
1917
2046
  useEffect(() => {
1918
2047
  void load();
@@ -2012,6 +2141,11 @@ function PluginsSection({ cwd }: { cwd: string }) {
2012
2141
  <div className="mt-1" style={{ fontSize: 13, color: "var(--dsw-label-tertiary)" }}>
2013
2142
  {t.pluginPageDesc}
2014
2143
  </div>
2144
+ {loadError && (
2145
+ <div role="alert" className="mt-3 rounded-xl px-3 py-2" style={{ fontSize: 12.5, color: "var(--dsw-danger)", background: "var(--dsw-hover)" }}>
2146
+ {loadError} <button type="button" className="underline" onClick={() => void load()}>{t.settingsRetry}</button>
2147
+ </div>
2148
+ )}
2015
2149
  </div>
2016
2150
 
2017
2151
  {toast && (
@@ -2092,8 +2226,11 @@ function PluginsSection({ cwd }: { cwd: string }) {
2092
2226
  try {
2093
2227
  const r = await fetch("/api/plugins", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "reload", cwd }) });
2094
2228
  const j = await r.json();
2095
- notify(j.success, j.success ? t.reloadedSessions.replace("{loaders}", String(j.data?.loaders ?? 0)).replace("{sessions}", String(j.data?.sessions ?? 0)) : j.error);
2096
- if (j.success) await load();
2229
+ if (!r.ok || !j.success) throw new Error(j.error || t.toastError);
2230
+ notify(true, t.reloadedSessions.replace("{loaders}", String(j.data?.loaders ?? 0)).replace("{sessions}", String(j.data?.sessions ?? 0)));
2231
+ await load();
2232
+ } catch (error) {
2233
+ notify(false, error instanceof Error ? error.message : t.toastError);
2097
2234
  } finally {
2098
2235
  setBusy(false);
2099
2236
  }