@rexvane/piweb 0.3.14 → 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 (262) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +12 -9
  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/required-server-files.js +3 -1
  7. package/.next/required-server-files.json +3 -1
  8. package/.next/routes-manifest.json +20 -0
  9. package/.next/server/app/_global-error/page.js +2 -2
  10. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_global-error.html +1 -1
  12. package/.next/server/app/_global-error.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page.js +1 -1
  17. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/_not-found.html +1 -1
  19. package/.next/server/app/_not-found.rsc +7 -4
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +7 -4
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -2
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +5 -2
  23. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  24. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  25. package/.next/server/app/api/agent/[id]/route.js +1 -1
  26. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  27. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  28. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  29. package/.next/server/app/api/agent/new/route.js +1 -1
  30. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  31. package/.next/server/app/api/config/route.js +1 -1
  32. package/.next/server/app/api/config/route.js.nft.json +1 -1
  33. package/.next/server/app/api/files/route.js +1 -1
  34. package/.next/server/app/api/files/route.js.nft.json +1 -1
  35. package/.next/server/app/api/git/route.js +2 -2
  36. package/.next/server/app/api/git/route.js.nft.json +1 -1
  37. package/.next/server/app/api/growth/route.js +1 -1
  38. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  39. package/.next/server/app/api/health/route.js +1 -1
  40. package/.next/server/app/api/models/route.js +1 -1
  41. package/.next/server/app/api/models/route.js.nft.json +1 -1
  42. package/.next/server/app/api/pi-settings/route.js +1 -1
  43. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  44. package/.next/server/app/api/plugins/route.js +1 -1
  45. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  46. package/.next/server/app/api/prompts/route.js +1 -1
  47. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  48. package/.next/server/app/api/security/route.js +1 -1
  49. package/.next/server/app/api/security/route.js.nft.json +1 -1
  50. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  51. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  52. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  53. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  54. package/.next/server/app/api/sessions/import/route.js +3 -3
  55. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  56. package/.next/server/app/api/sessions/route.js +1 -1
  57. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  58. package/.next/server/app/api/skills/route.js +2 -2
  59. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  60. package/.next/server/app/api/swarm/[id]/route.js +1 -0
  61. package/.next/server/app/api/swarm/[id]/route.js.nft.json +1 -0
  62. package/.next/server/app/api/swarm/[id]/route_client-reference-manifest.js +1 -0
  63. package/.next/server/app/api/swarm/route.js +1 -0
  64. package/.next/server/app/api/swarm/route.js.nft.json +1 -0
  65. package/.next/server/app/api/swarm/route_client-reference-manifest.js +1 -0
  66. package/.next/server/app/api/tests/route.js +2 -0
  67. package/.next/server/app/api/tests/route.js.nft.json +1 -0
  68. package/.next/server/app/api/tests/route_client-reference-manifest.js +1 -0
  69. package/.next/server/app/api/update/route.js +2 -2
  70. package/.next/server/app/api/update/route.js.nft.json +1 -1
  71. package/.next/server/app/api/version/route.js +1 -1
  72. package/.next/server/app/api/version/route.js.nft.json +1 -1
  73. package/.next/server/app/api/web-auth/route.js +1 -1
  74. package/.next/server/app/api/workspaces/route.js +1 -1
  75. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  76. package/.next/server/app/icon.svg/route.js +1 -1
  77. package/.next/server/app/index.html +1 -1
  78. package/.next/server/app/index.rsc +9 -6
  79. package/.next/server/app/index.segments/__PAGE__.segment.rsc +7 -4
  80. package/.next/server/app/index.segments/_full.segment.rsc +9 -6
  81. package/.next/server/app/index.segments/_tree.segment.rsc +5 -2
  82. package/.next/server/app/login/page.js +1 -1
  83. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/login.html +1 -1
  85. package/.next/server/app/login.rsc +8 -5
  86. package/.next/server/app/login.segments/_full.segment.rsc +8 -5
  87. package/.next/server/app/login.segments/_tree.segment.rsc +5 -2
  88. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +6 -3
  89. package/.next/server/app/page.js +1 -1
  90. package/.next/server/app/page_client-reference-manifest.js +1 -1
  91. package/.next/server/app-paths-manifest.json +12 -9
  92. package/.next/server/chunks/496.js +1 -1
  93. package/.next/server/chunks/532.js +1 -1
  94. package/.next/server/chunks/686.js +6 -2
  95. package/.next/server/chunks/757.js +1 -0
  96. package/.next/server/chunks/775.js +3 -0
  97. package/.next/server/middleware-build-manifest.js +1 -1
  98. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  99. package/.next/server/pages/404.html +1 -1
  100. package/.next/server/pages/500.html +1 -1
  101. package/.next/server/server-reference-manifest.json +1 -1
  102. package/.next/static/0t1jBUAuVaTFTXxRyIo3q/_buildManifest.js +1 -0
  103. package/.next/static/chunks/2608.43c58236c9cce8e6.js +1 -0
  104. package/.next/static/chunks/{2716.a9e808190a4e2424.js → 2716.4ec42df1d51354c9.js} +1 -1
  105. package/.next/static/chunks/4632.62139b565b657aac.js +1 -0
  106. package/.next/static/chunks/{4673.d3d0a7f2599080fb.js → 4673.7796104395deb8d7.js} +1 -1
  107. package/.next/static/chunks/4686-17f21a006739a596.js +1 -0
  108. package/.next/static/chunks/7577.10af9bb661652da8.js +1 -0
  109. package/.next/static/chunks/8866.38ec4ac4ee5a1b74.js +1 -0
  110. package/.next/static/chunks/app/_global-error/page-5148173b3f55e415.js +1 -0
  111. package/.next/static/chunks/app/api/agent/[id]/events/route-5148173b3f55e415.js +1 -0
  112. package/.next/static/chunks/app/api/agent/[id]/route-5148173b3f55e415.js +1 -0
  113. package/.next/static/chunks/app/api/agent/[id]/tree/route-5148173b3f55e415.js +1 -0
  114. package/.next/static/chunks/app/api/agent/new/route-5148173b3f55e415.js +1 -0
  115. package/.next/static/chunks/app/api/config/route-5148173b3f55e415.js +1 -0
  116. package/.next/static/chunks/app/api/files/route-5148173b3f55e415.js +1 -0
  117. package/.next/static/chunks/app/api/git/route-5148173b3f55e415.js +1 -0
  118. package/.next/static/chunks/app/api/growth/route-5148173b3f55e415.js +1 -0
  119. package/.next/static/chunks/app/api/health/route-5148173b3f55e415.js +1 -0
  120. package/.next/static/chunks/app/api/models/route-5148173b3f55e415.js +1 -0
  121. package/.next/static/chunks/app/api/pi-settings/route-5148173b3f55e415.js +1 -0
  122. package/.next/static/chunks/app/api/plugins/route-5148173b3f55e415.js +1 -0
  123. package/.next/static/chunks/app/api/prompts/route-5148173b3f55e415.js +1 -0
  124. package/.next/static/chunks/app/api/security/route-5148173b3f55e415.js +1 -0
  125. package/.next/static/chunks/app/api/sessions/[id]/export/route-5148173b3f55e415.js +1 -0
  126. package/.next/static/chunks/app/api/sessions/[id]/route-5148173b3f55e415.js +1 -0
  127. package/.next/static/chunks/app/api/sessions/import/route-5148173b3f55e415.js +1 -0
  128. package/.next/static/chunks/app/api/sessions/route-5148173b3f55e415.js +1 -0
  129. package/.next/static/chunks/app/api/skills/route-5148173b3f55e415.js +1 -0
  130. package/.next/static/chunks/app/api/swarm/[id]/route-5148173b3f55e415.js +1 -0
  131. package/.next/static/chunks/app/api/swarm/route-5148173b3f55e415.js +1 -0
  132. package/.next/static/chunks/app/api/tests/route-5148173b3f55e415.js +1 -0
  133. package/.next/static/chunks/app/api/update/route-5148173b3f55e415.js +1 -0
  134. package/.next/static/chunks/app/api/version/route-5148173b3f55e415.js +1 -0
  135. package/.next/static/chunks/app/api/web-auth/route-5148173b3f55e415.js +1 -0
  136. package/.next/static/chunks/app/api/workspaces/route-5148173b3f55e415.js +1 -0
  137. package/.next/static/chunks/app/page-14379b8060d5cf07.js +1 -0
  138. package/.next/static/chunks/next/dist/client/components/builtin/app-error-5148173b3f55e415.js +1 -0
  139. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-5148173b3f55e415.js +1 -0
  140. package/.next/static/chunks/next/dist/client/components/builtin/not-found-5148173b3f55e415.js +1 -0
  141. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-5148173b3f55e415.js +1 -0
  142. package/.next/static/chunks/webpack-d1a69ad9edb38b11.js +1 -0
  143. package/.next/static/css/d0a0162cb481abe8.css +1 -0
  144. package/.next/types/app/api/swarm/[id]/route.ts +353 -0
  145. package/.next/types/app/api/swarm/route.ts +353 -0
  146. package/.next/types/app/api/tests/route.ts +353 -0
  147. package/.next/types/routes.d.ts +4 -1
  148. package/.next/types/validator.ts +27 -0
  149. package/README.md +61 -7
  150. package/assets/archive.png +0 -0
  151. package/assets/growth-1.png +0 -0
  152. package/assets/growth-2.png +0 -0
  153. package/assets/import.png +0 -0
  154. package/assets/main.png +0 -0
  155. package/assets/outline.png +0 -0
  156. package/assets/prompts-1.png +0 -0
  157. package/assets/prompts-2.png +0 -0
  158. package/assets/providers-1.png +0 -0
  159. package/assets/providers-2.png +0 -0
  160. package/assets/providers-3.png +0 -0
  161. package/assets/trace-1.png +0 -0
  162. package/assets/trace-2.png +0 -0
  163. package/assets/trace-3.png +0 -0
  164. package/assets/trace-4.png +0 -0
  165. package/assets/viewer-1.png +0 -0
  166. package/assets/viewer-2.png +0 -0
  167. package/next.config.ts +6 -2
  168. package/package.json +10 -5
  169. package/scripts/launcher.mjs +1 -1
  170. package/scripts/managed-niubash-assets.mjs +146 -0
  171. package/scripts/pick-folder.ps1 +11 -2
  172. package/scripts/prepare-niubash-packages.mjs +182 -0
  173. package/scripts/release.mjs +11 -31
  174. package/scripts/run-test-file.mjs +34 -0
  175. package/scripts/smoke-managed-niubash.mjs +62 -0
  176. package/scripts/verify-package.mjs +53 -0
  177. package/scripts/verify-release-config.mjs +24 -0
  178. package/src/app/api/config/route.ts +4 -1
  179. package/src/app/api/security/route.ts +8 -3
  180. package/src/app/api/swarm/[id]/route.ts +28 -0
  181. package/src/app/api/swarm/route.ts +24 -0
  182. package/src/app/api/tests/route.ts +27 -0
  183. package/src/app/api/update/route.ts +8 -2
  184. package/src/app/globals.css +777 -0
  185. package/src/app/layout.tsx +13 -6
  186. package/src/components/AppShell.tsx +505 -199
  187. package/src/components/ChatInput.tsx +153 -26
  188. package/src/components/ChatWindow.tsx +46 -40
  189. package/src/components/ContextMeter.tsx +11 -17
  190. package/src/components/FileViewer.tsx +1 -1
  191. package/src/components/GitPanel.tsx +13 -11
  192. package/src/components/ModelSelector.tsx +19 -6
  193. package/src/components/PiMark.tsx +5 -38
  194. package/src/components/ProjectPanel.tsx +1 -0
  195. package/src/components/PromptPanel.tsx +132 -51
  196. package/src/components/SessionImportSection.tsx +53 -37
  197. package/src/components/SessionSidebar.tsx +159 -57
  198. package/src/components/SettingsPanel.tsx +330 -193
  199. package/src/components/SwarmCoordinatorPod.tsx +1021 -0
  200. package/src/components/TestInspectorPod.tsx +549 -0
  201. package/src/components/icons.tsx +23 -0
  202. package/src/hooks/usePiWeb.ts +112 -8
  203. package/src/i18n.tsx +59 -3
  204. package/src/lib/agent-manager.ts +182 -11
  205. package/src/lib/command-validation.ts +9 -1
  206. package/src/lib/delegation.ts +93 -0
  207. package/src/lib/managed-niubash.ts +231 -0
  208. package/src/lib/pi.ts +3 -2
  209. package/src/lib/runtime-activity.ts +27 -0
  210. package/src/lib/session-import/claude.ts +13 -2
  211. package/src/lib/session-import/codex.ts +44 -8
  212. package/src/lib/session-import/files.ts +14 -0
  213. package/src/lib/session-import/grok.ts +54 -24
  214. package/src/lib/session-import/index.ts +5 -0
  215. package/src/lib/session-import/lock.ts +25 -0
  216. package/src/lib/session-import/types.ts +12 -0
  217. package/src/lib/session-import/writer.ts +8 -3
  218. package/src/lib/skills-service.ts +16 -9
  219. package/src/lib/swarm-service.ts +371 -0
  220. package/src/lib/test-run-service.ts +122 -0
  221. package/src/lib/theme.ts +68 -1
  222. package/src/lib/trajectory.ts +1 -0
  223. package/src/lib/types.ts +14 -0
  224. package/src/lib/workflow.ts +55 -0
  225. package/.next/static/LOFl9bqqMV2goBV8erESO/_buildManifest.js +0 -1
  226. package/.next/static/chunks/2608.ec8ff1f5767c9056.js +0 -1
  227. package/.next/static/chunks/4632.112d2e893d48d188.js +0 -1
  228. package/.next/static/chunks/4686-7d694def920ff093.js +0 -1
  229. package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +0 -1
  230. package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +0 -1
  231. package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +0 -1
  232. package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +0 -1
  233. package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +0 -1
  234. package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +0 -1
  235. package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +0 -1
  236. package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +0 -1
  237. package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +0 -1
  238. package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +0 -1
  239. package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +0 -1
  240. package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +0 -1
  241. package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +0 -1
  242. package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +0 -1
  243. package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +0 -1
  244. package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +0 -1
  245. package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +0 -1
  246. package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +0 -1
  247. package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +0 -1
  248. package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +0 -1
  249. package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +0 -1
  250. package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +0 -1
  251. package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +0 -1
  252. package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +0 -1
  253. package/.next/static/chunks/app/page-9a5440047c5a415c.js +0 -1
  254. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +0 -1
  255. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +0 -1
  256. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +0 -1
  257. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +0 -1
  258. package/.next/static/chunks/webpack-d28e132e1b8c100c.js +0 -1
  259. package/.next/static/css/b2d35abc828d4713.css +0 -1
  260. package/assets/showcase.png +0 -0
  261. package/bin/pi.js +0 -50
  262. /package/.next/static/{LOFl9bqqMV2goBV8erESO → 0t1jBUAuVaTFTXxRyIo3q}/_ssgManifest.js +0 -0
@@ -34,46 +34,13 @@ export function PiMark({
34
34
  /** 品牌簇:对齐 dsh 品牌模板 [Logo] [Name] [BADGE] */
35
35
  export function BrandPi({ compact = false }: { compact?: boolean }) {
36
36
  if (compact) {
37
- return <PiMark size={18} />;
37
+ return <PiMark size={18} style={{ color: "var(--dsw-accent)" }} />;
38
38
  }
39
39
  return (
40
- <div
41
- className="inline-flex items-center select-none"
42
- style={{
43
- color: "var(--dsw-label-primary)",
44
- gap: 6,
45
- }}
46
- >
47
- <PiMark size={17} />
48
- <span
49
- className="inline-flex items-center"
50
- style={{
51
- fontSize: 16.5,
52
- fontWeight: 600,
53
- letterSpacing: "-0.02em",
54
- color: "var(--dsw-label-primary)",
55
- lineHeight: 1,
56
- }}
57
- >
58
- pi
59
- </span>
60
- <span
61
- className="inline-flex items-center justify-center font-bold"
62
- style={{
63
- fontSize: 9.5,
64
- letterSpacing: "0.06em",
65
- height: 15,
66
- paddingLeft: 4.5,
67
- paddingRight: 4.5,
68
- borderRadius: 4,
69
- background: "var(--dsw-label-primary)",
70
- color: "var(--dsw-bg-base)",
71
- lineHeight: 1,
72
- textTransform: "uppercase",
73
- }}
74
- >
75
- HARNESS
76
- </span>
40
+ <div className="pw-brand select-none">
41
+ <span className="pw-brand-symbol"><PiMark size={18} style={{ color: "var(--dsw-accent)" }} /></span>
42
+ <span className="pw-brand-name">piweb</span>
43
+ <span className="pw-brand-meta">CODE WORKBENCH</span>
77
44
  </div>
78
45
  );
79
46
  }
@@ -223,6 +223,7 @@ export function ProjectPanel({
223
223
  <input
224
224
  value={filter}
225
225
  onChange={(e) => setFilter(e.target.value)}
226
+ aria-label={t.growthFilter}
226
227
  placeholder={t.growthFilter}
227
228
  className="min-w-0 flex-1 bg-transparent"
228
229
  style={{ fontSize: 12 }}
@@ -6,7 +6,7 @@
6
6
  * 技能 / 提示模板,含扩展与 npm 包提供的那些);热会话额外给出"组装后的系统提示词"
7
7
  * 与每个工具的定义,它们不是文件、没有路径,直接在本面板内展开查看。
8
8
  */
9
- import { useCallback, useEffect, useState } from "react";
9
+ import { useCallback, useEffect, useRef, useState } from "react";
10
10
  import { useI18n } from "@/i18n";
11
11
  import { IconRefreshOutline14, IconCloseOutline14 } from "@/components/icons";
12
12
 
@@ -67,30 +67,46 @@ export function PromptPanel({
67
67
  onClose: () => void;
68
68
  }) {
69
69
  const { t } = useI18n();
70
- const [data, setData] = useState<PromptSourcesData | null>(null);
70
+ const owner = JSON.stringify([cwd, sessionId]);
71
+ const [catalog, setCatalog] = useState<{ owner: string; data: PromptSourcesData } | null>(null);
72
+ const data = catalog?.owner === owner ? catalog.data : null;
71
73
  const [error, setError] = useState<string | null>(null);
72
74
  const [busy, setBusy] = useState(false);
73
75
  const [opening, setOpening] = useState<string | null>(null);
76
+ const listRequest = useRef<AbortController | null>(null);
77
+ const readRequest = useRef<AbortController | null>(null);
74
78
 
75
79
  const load = useCallback(async () => {
80
+ listRequest.current?.abort();
81
+ readRequest.current?.abort();
82
+ const controller = new AbortController();
83
+ listRequest.current = controller;
84
+ const current = () => listRequest.current === controller && !controller.signal.aborted;
85
+ setCatalog(null);
86
+ setOpening(null);
76
87
  setBusy(true);
77
88
  setError(null);
78
89
  try {
79
90
  const query = new URLSearchParams({ cwd });
80
91
  if (sessionId) query.set("session", sessionId);
81
- const response = await fetch(`/api/prompts?${query.toString()}`);
92
+ const response = await fetch(`/api/prompts?${query.toString()}`, { signal: controller.signal });
82
93
  const body = await response.json();
94
+ if (!current()) return;
83
95
  if (!response.ok || !body.success) throw new Error(body.error || `request failed (${response.status})`);
84
- setData(body.data as PromptSourcesData);
96
+ setCatalog({ owner, data: body.data as PromptSourcesData });
85
97
  } catch (err) {
86
- setError(err instanceof Error ? err.message : String(err));
98
+ if (current()) setError(err instanceof Error ? err.message : String(err));
87
99
  } finally {
88
- setBusy(false);
100
+ if (current()) setBusy(false);
89
101
  }
90
- }, [cwd, sessionId]);
102
+ }, [cwd, sessionId, owner]);
91
103
 
92
104
  useEffect(() => {
93
105
  void load();
106
+ return () => {
107
+ listRequest.current?.abort();
108
+ readRequest.current?.abort();
109
+ };
94
110
  }, [load, refreshKey]);
95
111
 
96
112
  const originLabel = (origin: PromptSourceOrigin): string => {
@@ -115,21 +131,28 @@ export function PromptPanel({
115
131
  /** 文件类:按需读内容,交给外部查看器 */
116
132
  const openFile = async (source: PromptSource) => {
117
133
  if (!source.path || !onOpenContent) return;
134
+ readRequest.current?.abort();
135
+ const controller = new AbortController();
136
+ const catalogRequest = listRequest.current;
137
+ readRequest.current = controller;
138
+ const current = () => readRequest.current === controller && !controller.signal.aborted && listRequest.current === catalogRequest;
118
139
  setOpening(source.path);
119
140
  setError(null);
120
141
  try {
121
142
  const response = await fetch("/api/prompts", {
122
143
  method: "POST",
144
+ signal: controller.signal,
123
145
  headers: { "Content-Type": "application/json" },
124
146
  body: JSON.stringify({ action: "read", filePath: source.path, cwd }),
125
147
  });
126
148
  const body = await response.json();
149
+ if (!current()) return;
127
150
  if (!response.ok || !body.success) throw new Error(body.error || "read failed");
128
151
  onOpenContent(source.path, body.data.content as string);
129
152
  } catch (err) {
130
- setError(err instanceof Error ? err.message : String(err));
153
+ if (current()) setError(err instanceof Error ? err.message : String(err));
131
154
  } finally {
132
- setOpening(null);
155
+ if (current()) setOpening(null);
133
156
  }
134
157
  };
135
158
 
@@ -138,50 +161,94 @@ export function PromptPanel({
138
161
  );
139
162
  const hasTools = (data?.tools?.length ?? 0) > 0;
140
163
 
164
+ const fileExtBadge = (name: string): string => {
165
+ const ext = name.split(".").pop()?.toUpperCase() ?? "DOC";
166
+ return ext.length <= 4 ? ext : "DOC";
167
+ };
168
+
141
169
  return (
142
- <div className="flex min-h-0 flex-1 flex-col">
143
- <div className="hairline-b flex items-center gap-2 px-4 py-2.5">
144
- <span className="min-w-0 flex-1 truncate" style={{ fontSize: 13.5, fontWeight: 600 }}>{t.promptPanel}</span>
145
- <button type="button" className="icon-btn" style={{ width: 26, height: 26 }} title={t.promptRefresh} aria-label={t.promptRefresh} onClick={() => void load()}>
146
- <span className={busy ? "piweb-spin" : undefined} style={{ display: "inline-flex" }}>
147
- <IconRefreshOutline14 size={14} />
148
- </span>
149
- </button>
150
- <button type="button" className="icon-btn" style={{ width: 26, height: 26 }} title={t.close} aria-label={t.close} onClick={onClose}>
151
- <IconCloseOutline14 size={14} />
152
- </button>
170
+ <div className="flex min-h-0 flex-1 flex-col p-2 select-none">
171
+ {/* Right Header (Screen 2 Claymorphism) */}
172
+ <div className="flex items-center justify-between pb-3 mb-3 border-b border-slate-200 dark:border-slate-800 px-2">
173
+ <div className="flex items-center gap-2">
174
+ <div className="w-2.5 h-2.5 rounded-full bg-sky-500 animate-pulse" />
175
+ <h2 className="text-sm font-bold text-slate-900 dark:text-slate-100 tracking-tight">{t.promptPanel}</h2>
176
+ </div>
177
+ <div className="flex items-center gap-1.5 text-slate-500">
178
+ <button
179
+ type="button"
180
+ className="w-7 h-7 clay-btn bg-white dark:bg-slate-800 text-slate-700 hover:text-slate-950 dark:text-slate-300 dark:hover:text-white transition-colors shadow-sm"
181
+ title={t.promptRefresh}
182
+ aria-label={t.promptRefresh}
183
+ onClick={() => void load()}
184
+ >
185
+ <span className={busy ? "piweb-spin" : undefined} style={{ display: "inline-flex" }}>
186
+ <IconRefreshOutline14 size={13} />
187
+ </span>
188
+ </button>
189
+ <button
190
+ type="button"
191
+ className="w-7 h-7 clay-btn bg-white dark:bg-slate-800 text-slate-700 hover:text-rose-600 dark:text-slate-300 transition-colors shadow-sm"
192
+ title={t.close}
193
+ aria-label={t.close}
194
+ onClick={onClose}
195
+ >
196
+ <IconCloseOutline14 size={13} />
197
+ </button>
198
+ </div>
153
199
  </div>
154
- <div className="min-h-0 flex-1 overflow-auto px-3 py-3" style={{ fontSize: 12.5 }}>
200
+
201
+ <div className="min-h-0 flex-1 overflow-y-auto px-1 space-y-4 text-xs">
155
202
  {error && (
156
- <div className="mb-2 rounded-lg px-2 py-1.5" style={{ background: "var(--dsw-hover)", color: "var(--dsw-danger)" }} role="alert">
203
+ <div className="rounded-xl px-3 py-2 bg-rose-50 text-rose-700 dark:bg-rose-950/40 dark:text-rose-300 font-semibold" role="alert">
157
204
  {error}
158
205
  </div>
159
206
  )}
160
- {!data && busy && <div style={{ color: "var(--dsw-label-tertiary)" }}>{t.promptLoading}</div>}
207
+
208
+ {!data && busy && <div className="text-slate-500 py-4 text-center">{t.promptLoading}</div>}
209
+
161
210
  {data && !data.sessionReady && (
162
- <div className="mb-2 rounded-lg px-2 py-1.5" style={{ background: "var(--dsw-hover)", color: "var(--dsw-label-caption)" }} role="status">
211
+ <div className="rounded-xl px-3 py-2.5 clay-inset text-slate-600 dark:text-slate-300 leading-relaxed text-xs" role="status">
163
212
  {t.promptColdHint}
164
213
  </div>
165
214
  )}
166
215
 
167
216
  {groups.map((group) => (
168
- <section key={group.kind} className="mb-3">
169
- <div className="mb-1 px-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>{kindLabel(group.kind)}</div>
170
- <div className="flex flex-col gap-1">
217
+ <section key={group.kind} className="space-y-1.5">
218
+ <div className="flex items-center justify-between text-xs font-bold text-slate-800 dark:text-slate-200 px-1 mb-1">
219
+ <span>{kindLabel(group.kind)}</span>
220
+ <span className="text-[10px] text-slate-500 dark:text-slate-400 font-mono font-semibold">{group.items.length} 项</span>
221
+ </div>
222
+ <div className="flex flex-col gap-1.5">
171
223
  {group.items.map((source) => (
172
224
  <button
173
225
  key={`${source.kind}:${source.path ?? source.name}`}
174
226
  type="button"
175
- className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left"
176
- style={{ background: opening === source.path ? "var(--dsw-active)" : "transparent", border: "0.5px solid var(--dsw-border-l2)" }}
227
+ className={`clay-card p-3 flex items-center justify-between hover:scale-[1.01] transition-all cursor-pointer rounded-2xl text-left bg-white dark:bg-slate-900 shadow-sm border border-slate-200 dark:border-slate-800 ${
228
+ opening === source.path ? "ring-2 ring-emerald-500" : ""
229
+ }`}
177
230
  title={source.path ?? source.name}
178
231
  onClick={() => void openFile(source)}
179
232
  >
180
- <span className="min-w-0 flex-1 truncate" style={{ color: "var(--dsw-label-primary)" }}>{source.name}</span>
181
- <span className="flex-none" style={{ fontSize: 10.5, color: "var(--dsw-label-tertiary)" }}>{formatBytes(source.bytes)}</span>
182
- <span className="flex-none rounded-md px-1.5 py-0.5" style={{ fontSize: 10.5, border: "0.5px solid var(--dsw-border-l3)", color: "var(--dsw-label-tertiary)" }}>
183
- {originLabel(source.origin)}
184
- </span>
233
+ <div className="flex items-center gap-2.5 min-w-0 flex-1 mr-2">
234
+ <div className="w-7 h-7 clay-inset bg-emerald-50 text-emerald-700 dark:bg-emerald-950/70 dark:text-emerald-300 flex items-center justify-center rounded-xl font-mono font-bold text-[10px] flex-none">
235
+ {fileExtBadge(source.name)}
236
+ </div>
237
+ <div className="min-w-0 flex-1">
238
+ <div className="text-xs font-bold text-slate-900 dark:text-slate-100 truncate">{source.name}</div>
239
+ <div className="text-[11px] text-slate-600 dark:text-slate-400 truncate">{source.path ?? kindLabel(source.kind)}</div>
240
+ </div>
241
+ </div>
242
+ <div className="text-right flex-none flex flex-col items-end gap-0.5">
243
+ {source.bytes !== undefined && (
244
+ <span className="text-[11px] font-mono font-bold text-slate-800 dark:text-slate-200 block">
245
+ {formatBytes(source.bytes)}
246
+ </span>
247
+ )}
248
+ <span className="text-[10px] px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-semibold border border-slate-200 dark:border-slate-700">
249
+ {originLabel(source.origin)}
250
+ </span>
251
+ </div>
185
252
  </button>
186
253
  ))}
187
254
  </div>
@@ -189,15 +256,17 @@ export function PromptPanel({
189
256
  ))}
190
257
 
191
258
  {hasTools && (
192
- <section className="mb-3">
193
- <div className="mb-1 px-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>{t.promptKindTool}</div>
194
- <div className="flex flex-col gap-1">
259
+ <section className="space-y-1.5">
260
+ <div className="flex items-center justify-between text-xs font-bold text-slate-800 dark:text-slate-200 px-1 mb-1">
261
+ <span>{t.promptKindTool}</span>
262
+ <span className="text-[10px] text-slate-500 dark:text-slate-400 font-mono font-semibold">{data!.tools!.length} 个工具</span>
263
+ </div>
264
+ <div className="flex flex-col gap-1.5">
195
265
  {data!.tools!.map((tool) => (
196
266
  <button
197
267
  key={`tool:${tool.name}`}
198
268
  type="button"
199
- className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left"
200
- style={{ border: "0.5px solid var(--dsw-border-l2)" }}
269
+ className="clay-card p-3 flex items-center justify-between hover:scale-[1.01] transition-all cursor-pointer rounded-2xl text-left bg-white dark:bg-slate-900 shadow-sm border border-slate-200 dark:border-slate-800"
201
270
  title={tool.name}
202
271
  onClick={() =>
203
272
  onOpenContent?.(
@@ -214,8 +283,16 @@ export function PromptPanel({
214
283
  )
215
284
  }
216
285
  >
217
- <span className="min-w-0 flex-1 truncate" style={{ color: "var(--dsw-label-primary)" }}>{tool.name}</span>
218
- <span className="flex-none rounded-md px-1.5 py-0.5" style={{ fontSize: 10.5, border: "0.5px solid var(--dsw-border-l3)", color: "var(--dsw-label-tertiary)" }}>
286
+ <div className="flex items-center gap-2.5 min-w-0 flex-1 mr-2">
287
+ <div className="w-7 h-7 clay-inset bg-sky-50 text-sky-700 dark:bg-sky-950/70 dark:text-sky-300 flex items-center justify-center rounded-xl font-mono font-bold text-[10px] flex-none">
288
+ TOOL
289
+ </div>
290
+ <div className="min-w-0 flex-1">
291
+ <div className="text-xs font-bold text-slate-900 dark:text-slate-100 truncate">{tool.name}</div>
292
+ <div className="text-[11px] text-slate-600 dark:text-slate-400 truncate">{tool.description || t.promptKindTool}</div>
293
+ </div>
294
+ </div>
295
+ <span className="text-[10px] px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-semibold border border-slate-200 dark:border-slate-700 flex-none">
219
296
  {tool.source === "builtin" ? t.promptToolBuiltin : t.promptToolExtension}
220
297
  </span>
221
298
  </button>
@@ -225,31 +302,35 @@ export function PromptPanel({
225
302
  )}
226
303
 
227
304
  {data?.assembledSystemPrompt && (
228
- <section className="mb-3">
229
- <div className="mb-1 px-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>{t.promptKindAssembled}</div>
305
+ <section className="space-y-1.5">
306
+ <div className="text-xs font-bold text-slate-800 dark:text-slate-200 px-1 mb-1">{t.promptKindAssembled}</div>
230
307
  <button
231
308
  type="button"
232
- className="w-full rounded-lg px-2 py-1.5 text-left"
233
- style={{ border: "0.5px solid var(--dsw-border-l2)" }}
309
+ className="w-full clay-card p-3 flex items-center gap-2.5 hover:scale-[1.01] transition-all cursor-pointer rounded-2xl text-left bg-white dark:bg-slate-900 shadow-sm border border-slate-200 dark:border-slate-800"
234
310
  title={t.promptAssembledKey}
235
311
  onClick={() => onOpenContent?.("system-prompt.md", data.assembledSystemPrompt ?? "")}
236
312
  >
237
- <span className="truncate" style={{ color: "var(--dsw-label-primary)" }}>{t.promptAssembledKey}</span>
313
+ <div className="w-7 h-7 clay-inset bg-emerald-50 text-emerald-700 dark:bg-emerald-950/70 dark:text-emerald-300 flex items-center justify-center rounded-xl font-bold text-sm flex-none">
314
+ π
315
+ </div>
316
+ <span className="truncate font-bold text-slate-900 dark:text-slate-100 text-xs">{t.promptAssembledKey}</span>
238
317
  </button>
239
318
  </section>
240
319
  )}
241
320
 
242
321
  {data?.compactedSummary && (
243
- <section className="mb-3">
244
- <div className="mb-1 px-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>{t.promptKindCompacted}</div>
322
+ <section className="space-y-1.5">
323
+ <div className="text-xs font-bold text-slate-800 dark:text-slate-200 px-1 mb-1">{t.promptKindCompacted}</div>
245
324
  <button
246
325
  type="button"
247
- className="w-full rounded-lg px-2 py-1.5 text-left"
248
- style={{ border: "0.5px solid var(--dsw-border-l2)" }}
326
+ className="w-full clay-card p-3 flex items-center gap-2.5 hover:scale-[1.01] transition-all cursor-pointer rounded-2xl text-left bg-white dark:bg-slate-900 shadow-sm border border-slate-200 dark:border-slate-800"
249
327
  title={t.promptCompactedKey}
250
328
  onClick={() => onOpenContent?.("compacted-summary.md", data.compactedSummary ?? "")}
251
329
  >
252
- <span className="truncate" style={{ color: "var(--dsw-label-primary)" }}>{t.promptCompactedKey}</span>
330
+ <div className="w-7 h-7 clay-inset bg-amber-50 text-amber-700 dark:bg-amber-950/70 dark:text-amber-300 flex items-center justify-center rounded-xl font-bold text-xs flex-none">
331
+ SUM
332
+ </div>
333
+ <span className="truncate font-bold text-slate-900 dark:text-slate-100 text-xs">{t.promptCompactedKey}</span>
253
334
  </button>
254
335
  </section>
255
336
  )}
@@ -36,6 +36,8 @@ interface ImportOutcome {
36
36
  reason?: string;
37
37
  messageCount?: number;
38
38
  workspace?: string;
39
+ /** 已成功导入,但源里的运行时注入/加密内容等无法作为对话还原 */
40
+ skipped?: string[];
39
41
  }
40
42
 
41
43
  interface ImportReport {
@@ -60,6 +62,8 @@ function dayOf(ms?: number): string {
60
62
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
61
63
  }
62
64
 
65
+ const summaryKey = (summary: Pick<Summary, "source" | "externalId">) => `${summary.source}:${summary.externalId}`;
66
+
63
67
  export function SessionImportSection({ cwd }: { cwd: string }) {
64
68
  const { t } = useI18n();
65
69
  const [summaries, setSummaries] = useState<Summary[]>([]);
@@ -83,9 +87,14 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
83
87
  setFailure(json.error || t.toastError);
84
88
  return;
85
89
  }
86
- setSummaries(json.data.sessions ?? []);
90
+ const nextSummaries = (json.data.sessions ?? []) as Summary[];
91
+ const nextImported = new Set<string>(json.data.imported ?? []);
92
+ const selectable = new Set(nextSummaries.map(summaryKey));
93
+ setSummaries(nextSummaries);
87
94
  setErrors(json.data.errors ?? []);
88
- setImportedKeys(new Set<string>(json.data.imported ?? []));
95
+ setImportedKeys(nextImported);
96
+ // 重新扫描后,源会话可能已经消失或被另一个请求导入;不要留下幽灵选择
97
+ setSelected((previous) => new Set([...previous].filter((item) => selectable.has(item) && !nextImported.has(item))));
89
98
  } catch (error) {
90
99
  setFailure(error instanceof Error ? error.message : String(error));
91
100
  } finally {
@@ -109,8 +118,6 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
109
118
  void scan();
110
119
  }, [scan]);
111
120
 
112
- const key = (summary: Summary) => `${summary.source}:${summary.externalId}`;
113
-
114
121
  const groups = useMemo(() => {
115
122
  const bySource = new Map<string, Summary[]>();
116
123
  for (const summary of summaries) {
@@ -120,27 +127,30 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
120
127
  }
121
128
  return SOURCE_ORDER.filter((source) => bySource.has(source)).map((source) => {
122
129
  const rows = [...(bySource.get(source) as Summary[])].sort((left, right) => (right.updatedAt ?? right.createdAt ?? 0) - (left.updatedAt ?? left.createdAt ?? 0));
123
- return { source, rows, imported: rows.filter((row) => importedKeys.has(key(row))).length };
130
+ return { source, rows, imported: rows.filter((row) => importedKeys.has(summaryKey(row))).length };
124
131
  });
125
132
  }, [summaries, importedKeys]);
126
133
 
127
134
  /**
128
135
  * 一次只显示一个来源:六个工具各列 15 条就是 90 行,全铺出来既难找也没必要。
129
- * 默认落在第一个有会话的来源上(SOURCE_ORDER 的顺序),用户切换后不被扫描结果重置。
136
+ * 首次打开默认落到“最近活动会话”所在的来源;用户切换后保持选择。
137
+ * 直接派生有效来源,不用 effect 回填,避免初始化 effect 覆盖用户刚完成的点击。
130
138
  */
131
139
  const [activeSource, setActiveSource] = useState("");
132
- useEffect(() => {
133
- if (activeSource && groups.some((group) => group.source === activeSource)) return;
134
- setActiveSource(groups[0]?.source ?? "");
135
- }, [groups, activeSource]);
136
- const activeGroup = groups.find((group) => group.source === activeSource);
140
+ const defaultSource = groups.reduce<{ source: string; at: number } | null>((latest, group) => {
141
+ const first = group.rows[0];
142
+ const at = first?.updatedAt ?? first?.createdAt ?? 0;
143
+ return !latest || at > latest.at ? { source: group.source, at } : latest;
144
+ }, null)?.source ?? "";
145
+ const visibleSource = activeSource && groups.some((group) => group.source === activeSource) ? activeSource : defaultSource;
146
+ const activeGroup = groups.find((group) => group.source === visibleSource);
137
147
 
138
148
  const toggle = (summary: Summary) => {
139
- if (importedKeys.has(key(summary))) return;
149
+ if (importedKeys.has(summaryKey(summary))) return;
140
150
  setSelected((previous) => {
141
151
  const next = new Set(previous);
142
- if (next.has(key(summary))) next.delete(key(summary));
143
- else next.add(key(summary));
152
+ if (next.has(summaryKey(summary))) next.delete(summaryKey(summary));
153
+ else next.add(summaryKey(summary));
144
154
  return next;
145
155
  });
146
156
  };
@@ -149,9 +159,9 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
149
159
  setSelected((previous) => {
150
160
  const next = new Set(previous);
151
161
  for (const row of rows) {
152
- if (importedKeys.has(key(row))) continue;
153
- if (checked) next.add(key(row));
154
- else next.delete(key(row));
162
+ if (importedKeys.has(summaryKey(row))) continue;
163
+ if (checked) next.add(summaryKey(row));
164
+ else next.delete(summaryKey(row));
155
165
  }
156
166
  return next;
157
167
  });
@@ -159,7 +169,7 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
159
169
 
160
170
  const run = async () => {
161
171
  if (!selected.size) return;
162
- const items = summaries.filter((summary) => selected.has(key(summary))).map((summary) => ({ source: summary.source, externalId: summary.externalId }));
172
+ const items = summaries.filter((summary) => selected.has(summaryKey(summary))).map((summary) => ({ source: summary.source, externalId: summary.externalId }));
163
173
  setImporting(true);
164
174
  setFailure(null);
165
175
  setReport(null);
@@ -198,6 +208,7 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
198
208
 
199
209
  const failures = report?.results.filter((result) => result.status === "failed") ?? [];
200
210
  const skipped = report?.results.filter((result) => result.status === "skipped") ?? [];
211
+ const importedWithNotes = report?.results.filter((result) => result.status === "imported" && result.skipped?.length) ?? [];
201
212
 
202
213
  return (
203
214
  <div className="flex flex-col gap-4" data-testid="session-import">
@@ -277,6 +288,11 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
277
288
  {sourceLabel(result.source)} · {result.title || result.externalId} — {result.reason}
278
289
  </div>
279
290
  ))}
291
+ {importedWithNotes.map((result) => (
292
+ <div key={`note-${result.source}-${result.externalId}`} className="mt-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
293
+ {sourceLabel(result.source)} · {result.title || result.externalId} — {result.skipped?.join(";")}
294
+ </div>
295
+ ))}
280
296
  {failures.map((result) => (
281
297
  <div key={`fail-${result.source}-${result.externalId}`} className="mt-1" style={{ fontSize: 11.5, color: "var(--dsw-danger)" }}>
282
298
  {sourceLabel(result.source)} · {result.title || result.externalId} — {result.reason}
@@ -289,27 +305,25 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
289
305
 
290
306
  {/* 来源选择器:选中哪个就只列哪个工具的会话 */}
291
307
  {groups.length > 0 && (
292
- <div className="flex flex-wrap items-center gap-1.5" role="tablist" aria-label={t.importPickSource} data-testid="import-source-tabs">
308
+ <div className="flex flex-wrap items-center gap-2 p-1.5 rounded-2xl bg-slate-100/80 dark:bg-slate-900/60" role="tablist" aria-label={t.importPickSource} data-testid="import-source-tabs">
293
309
  {groups.map((group) => {
294
- const active = group.source === activeSource;
310
+ const active = group.source === visibleSource;
295
311
  return (
296
312
  <button
297
313
  key={group.source}
298
314
  type="button"
299
315
  role="tab"
300
316
  aria-selected={active}
301
- className="rounded-full px-3 py-1 transition-colors"
302
- style={{
303
- fontSize: 12,
304
- border: `0.5px solid ${active ? "var(--dsw-accent)" : "var(--dsw-border-l2)"}`,
305
- background: active ? "var(--dsw-accent)" : "transparent",
306
- color: active ? "#fff" : "var(--dsw-label-tertiary)",
307
- }}
317
+ className={`clay-btn rounded-xl px-3.5 py-1.5 transition-all text-xs font-medium flex items-center gap-1.5 ${
318
+ active
319
+ ? "clay-btn-peach text-white shadow-sm font-semibold"
320
+ : "clay-btn-soft text-slate-600 dark:text-slate-300"
321
+ }`}
308
322
  onClick={() => setActiveSource(group.source)}
309
323
  data-testid={`import-source-${group.source}`}
310
324
  >
311
- {sourceLabel(group.source)}
312
- <span style={{ marginLeft: 6, opacity: 0.75 }}>{group.rows.length}</span>
325
+ <span>{sourceLabel(group.source)}</span>
326
+ <span className={`text-[10px] px-1.5 py-0.2 rounded-full ${active ? "bg-white/30 text-white" : "bg-slate-200 dark:bg-slate-700 text-slate-500"}`}>{group.rows.length}</span>
313
327
  </button>
314
328
  );
315
329
  })}
@@ -323,35 +337,37 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
323
337
  {t.importGroupCount.replace("{n}", String(activeGroup.rows.length)).replace("{imported}", String(activeGroup.imported))}
324
338
  </span>
325
339
  <div className="flex-1" />
326
- <button className="rounded-lg px-2 py-0.5" style={{ fontSize: 11.5, color: "var(--dsw-label-tertiary)" }} onClick={() => selectGroup(activeGroup.rows, true)}>
340
+ <button className="clay-btn clay-btn-soft rounded-lg px-2.5 py-1 text-xs" onClick={() => selectGroup(activeGroup.rows, true)}>
327
341
  {t.importSelectAll}
328
342
  </button>
329
- <button className="rounded-lg px-2 py-0.5" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }} onClick={() => selectGroup(activeGroup.rows, false)}>
343
+ <button className="clay-btn clay-btn-soft rounded-lg px-2.5 py-1 text-xs text-slate-400" onClick={() => selectGroup(activeGroup.rows, false)}>
330
344
  {t.importSelectNone}
331
345
  </button>
332
346
  </div>
333
347
  <div className="flex flex-col gap-1.5">
334
348
  {activeGroup.rows.map((row) => {
335
- const rowKey = key(row);
349
+ const rowKey = summaryKey(row);
336
350
  const done = importedKeys.has(rowKey);
337
351
  const checked = selected.has(rowKey);
338
352
  return (
339
353
  <label
340
354
  key={rowKey}
341
- className="flex cursor-pointer items-center gap-3 rounded-xl px-3 py-2"
342
- style={{ border: `0.5px solid ${checked ? "var(--dsw-accent)" : "var(--dsw-border-l2)"}`, opacity: done ? 0.55 : 1 }}
355
+ className={`clay-card flex cursor-pointer items-center gap-3 rounded-2xl px-3.5 py-2.5 transition-all hover:translate-x-0.5 ${
356
+ checked ? "ring-2 ring-emerald-500 bg-white dark:bg-slate-800" : "bg-white/70 dark:bg-slate-900/60"
357
+ }`}
358
+ style={{ opacity: done ? 0.55 : 1 }}
343
359
  >
344
360
  <input type="checkbox" checked={checked} disabled={done} onChange={() => toggle(row)} />
345
361
  <div className="min-w-0 flex-1">
346
- <div className="truncate" style={{ fontSize: 12.5 }} title={row.title}>
362
+ <div className="truncate text-xs font-medium text-slate-800 dark:text-slate-100" title={row.title}>
347
363
  {row.title || t.importUntitled}
348
364
  </div>
349
- <div className="truncate" style={{ fontSize: 11, color: "var(--dsw-label-caption)" }} title={row.projectPath}>
365
+ <div className="truncate text-[11px] text-slate-400" title={row.projectPath}>
350
366
  {row.projectPath || t.importNoProject}
351
367
  </div>
352
368
  </div>
353
369
  {done && (
354
- <span className="rounded-full px-2 py-0.5" style={{ fontSize: 10, background: "var(--dsw-hover)", color: "var(--dsw-label-caption)" }}>
370
+ <span className="clay-badge rounded-full px-2 py-0.5 text-[10px] bg-slate-100 text-slate-400">
355
371
  {t.importAlready}
356
372
  </span>
357
373
  )}