@rexvane/piweb 0.3.11 → 0.3.13

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 (209) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +8 -6
  3. package/.next/build-manifest.json +6 -6
  4. package/.next/prerender-manifest.json +7 -7
  5. package/.next/react-loadable-manifest.json +7 -7
  6. package/.next/routes-manifest.json +12 -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 +1 -1
  18. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  20. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  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/files/route.js +1 -1
  31. package/.next/server/app/api/git/route.js +2 -2
  32. package/.next/server/app/api/growth/route.js +1 -1
  33. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  34. package/.next/server/app/api/health/route.js +1 -1
  35. package/.next/server/app/api/models/route.js +1 -1
  36. package/.next/server/app/api/models/route.js.nft.json +1 -1
  37. package/.next/server/app/api/pi-settings/route.js +1 -1
  38. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  39. package/.next/server/app/api/plugins/route.js +1 -1
  40. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  41. package/.next/server/app/api/prompts/route.js +1 -0
  42. package/.next/server/app/api/prompts/route.js.nft.json +1 -0
  43. package/.next/server/app/api/prompts/route_client-reference-manifest.js +1 -0
  44. package/.next/server/app/api/security/route.js +1 -1
  45. package/.next/server/app/api/security/route.js.nft.json +1 -1
  46. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  47. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  48. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  49. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  50. package/.next/server/app/api/sessions/import/route.js +6 -0
  51. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -0
  52. package/.next/server/app/api/sessions/import/route_client-reference-manifest.js +1 -0
  53. package/.next/server/app/api/sessions/route.js +1 -1
  54. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  55. package/.next/server/app/api/skills/route.js +2 -2
  56. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  57. package/.next/server/app/api/update/route.js +2 -2
  58. package/.next/server/app/api/update/route.js.nft.json +1 -1
  59. package/.next/server/app/api/version/route.js +1 -1
  60. package/.next/server/app/api/web-auth/route.js +1 -1
  61. package/.next/server/app/api/workspaces/route.js +1 -1
  62. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  63. package/.next/server/app/icon.svg/route.js +1 -1
  64. package/.next/server/app/index.html +1 -1
  65. package/.next/server/app/index.rsc +3 -3
  66. package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
  67. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  68. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  69. package/.next/server/app/login/page.js +1 -1
  70. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  71. package/.next/server/app/login.html +1 -1
  72. package/.next/server/app/login.rsc +2 -2
  73. package/.next/server/app/login.segments/_full.segment.rsc +2 -2
  74. package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
  75. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  76. package/.next/server/app/page.js +1 -1
  77. package/.next/server/app/page_client-reference-manifest.js +1 -1
  78. package/.next/server/app-paths-manifest.json +8 -6
  79. package/.next/server/chunks/496.js +1 -1
  80. package/.next/server/chunks/532.js +1 -0
  81. package/.next/server/chunks/686.js +2 -2
  82. package/.next/server/middleware-build-manifest.js +1 -1
  83. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  84. package/.next/server/pages/404.html +1 -1
  85. package/.next/server/pages/500.html +1 -1
  86. package/.next/server/server-reference-manifest.json +1 -1
  87. package/.next/static/chunks/2155.2f27fe7098987699.js +1 -0
  88. package/.next/static/chunks/2608.ec8ff1f5767c9056.js +1 -0
  89. package/.next/static/chunks/2716.a9e808190a4e2424.js +1 -0
  90. package/.next/static/chunks/{794-d1a40dcf24d651d9.js → 3794-3679f1c05160977c.js} +1 -1
  91. package/.next/static/chunks/4632.d574c210ad78d065.js +1 -0
  92. package/.next/static/chunks/4673.d3d0a7f2599080fb.js +1 -0
  93. package/.next/static/chunks/4686-0a4155f8488314b8.js +1 -0
  94. package/.next/static/chunks/4bd1b696-93bf506b236d6248.js +1 -0
  95. package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +1 -0
  96. package/.next/static/chunks/app/_not-found/page-2d40d1d7d916666a.js +1 -0
  97. package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +1 -0
  98. package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +1 -0
  99. package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +1 -0
  100. package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +1 -0
  101. package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +1 -0
  102. package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +1 -0
  103. package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +1 -0
  104. package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +1 -0
  105. package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +1 -0
  106. package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +1 -0
  107. package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +1 -0
  108. package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +1 -0
  109. package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +1 -0
  110. package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +1 -0
  111. package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +1 -0
  112. package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +1 -0
  113. package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +1 -0
  114. package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +1 -0
  115. package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +1 -0
  116. package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +1 -0
  117. package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +1 -0
  118. package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +1 -0
  119. package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +1 -0
  120. package/.next/static/chunks/app/layout-50d06e2078d6ce76.js +1 -0
  121. package/.next/static/chunks/app/login/page-52ef4841cbd199a8.js +1 -0
  122. package/.next/static/chunks/app/page-9a5440047c5a415c.js +1 -0
  123. package/.next/static/chunks/framework-c4e651fe89edfa91.js +1 -0
  124. package/.next/static/chunks/{main-a315d7c58b599962.js → main-6159b22a8cb0b7c5.js} +2 -2
  125. package/.next/static/chunks/main-app-e9b53f0e1c6591b2.js +1 -0
  126. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +1 -0
  127. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +1 -0
  128. package/.next/static/chunks/next/dist/client/components/builtin/global-error-a7e260f8cc2582d7.js +1 -0
  129. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +1 -0
  130. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +1 -0
  131. package/.next/static/chunks/webpack-b1a572e319353f5d.js +1 -0
  132. package/.next/static/olV7jxKRN5VhfEK012WhV/_buildManifest.js +1 -0
  133. package/.next/types/app/api/prompts/route.ts +353 -0
  134. package/.next/types/app/api/sessions/import/route.ts +353 -0
  135. package/.next/types/routes.d.ts +3 -1
  136. package/.next/types/validator.ts +18 -0
  137. package/README.md +4 -0
  138. package/package.json +1 -1
  139. package/src/app/api/prompts/route.ts +51 -0
  140. package/src/app/api/sessions/import/route.ts +74 -0
  141. package/src/components/AppShell.tsx +42 -19
  142. package/src/components/ChatWindow.tsx +20 -35
  143. package/src/components/PromptPanel.tsx +259 -0
  144. package/src/components/SessionImportSection.tsx +333 -0
  145. package/src/components/SettingsPanel.tsx +63 -57
  146. package/src/i18n.tsx +98 -4
  147. package/src/lib/agent-manager.ts +35 -8
  148. package/src/lib/clipboard.ts +27 -0
  149. package/src/lib/command-validation.ts +1 -0
  150. package/src/lib/prompt-sources.ts +229 -0
  151. package/src/lib/session-import/claude.ts +219 -0
  152. package/src/lib/session-import/codex.ts +206 -0
  153. package/src/lib/session-import/dsh-zstd.ts +69 -0
  154. package/src/lib/session-import/dsh.ts +248 -0
  155. package/src/lib/session-import/files.ts +84 -0
  156. package/src/lib/session-import/grok.ts +166 -0
  157. package/src/lib/session-import/index.ts +229 -0
  158. package/src/lib/session-import/oc-family.ts +275 -0
  159. package/src/lib/session-import/opencode.ts +7 -0
  160. package/src/lib/session-import/paths.ts +37 -0
  161. package/src/lib/session-import/sqlite.ts +89 -0
  162. package/src/lib/session-import/types.ts +171 -0
  163. package/src/lib/session-import/writer.ts +259 -0
  164. package/src/lib/session-import/zcode.ts +8 -0
  165. package/src/lib/types.ts +1 -0
  166. package/.next/server/chunks/118.js +0 -1
  167. package/.next/static/222KLUIvefiGf7M0Ov-Qz/_buildManifest.js +0 -1
  168. package/.next/static/chunks/127.c31e688e156605e3.js +0 -1
  169. package/.next/static/chunks/155.a3c40ae481da1358.js +0 -1
  170. package/.next/static/chunks/4bd1b696-92152b0f5947070d.js +0 -1
  171. package/.next/static/chunks/608.5b8aae42a9998ee7.js +0 -1
  172. package/.next/static/chunks/673.93445c8600bc1dd9.js +0 -1
  173. package/.next/static/chunks/686-9f15e4e370d26815.js +0 -1
  174. package/.next/static/chunks/716.5266fe10b5578591.js +0 -1
  175. package/.next/static/chunks/app/_global-error/page-1dac1b3ca8adb975.js +0 -1
  176. package/.next/static/chunks/app/_not-found/page-9de30f6f614fce02.js +0 -1
  177. package/.next/static/chunks/app/api/agent/[id]/events/route-1dac1b3ca8adb975.js +0 -1
  178. package/.next/static/chunks/app/api/agent/[id]/route-1dac1b3ca8adb975.js +0 -1
  179. package/.next/static/chunks/app/api/agent/[id]/tree/route-1dac1b3ca8adb975.js +0 -1
  180. package/.next/static/chunks/app/api/agent/new/route-1dac1b3ca8adb975.js +0 -1
  181. package/.next/static/chunks/app/api/config/route-1dac1b3ca8adb975.js +0 -1
  182. package/.next/static/chunks/app/api/files/route-1dac1b3ca8adb975.js +0 -1
  183. package/.next/static/chunks/app/api/git/route-1dac1b3ca8adb975.js +0 -1
  184. package/.next/static/chunks/app/api/growth/route-1dac1b3ca8adb975.js +0 -1
  185. package/.next/static/chunks/app/api/health/route-1dac1b3ca8adb975.js +0 -1
  186. package/.next/static/chunks/app/api/models/route-1dac1b3ca8adb975.js +0 -1
  187. package/.next/static/chunks/app/api/pi-settings/route-1dac1b3ca8adb975.js +0 -1
  188. package/.next/static/chunks/app/api/plugins/route-1dac1b3ca8adb975.js +0 -1
  189. package/.next/static/chunks/app/api/security/route-1dac1b3ca8adb975.js +0 -1
  190. package/.next/static/chunks/app/api/sessions/[id]/export/route-1dac1b3ca8adb975.js +0 -1
  191. package/.next/static/chunks/app/api/sessions/[id]/route-1dac1b3ca8adb975.js +0 -1
  192. package/.next/static/chunks/app/api/sessions/route-1dac1b3ca8adb975.js +0 -1
  193. package/.next/static/chunks/app/api/skills/route-1dac1b3ca8adb975.js +0 -1
  194. package/.next/static/chunks/app/api/update/route-1dac1b3ca8adb975.js +0 -1
  195. package/.next/static/chunks/app/api/version/route-1dac1b3ca8adb975.js +0 -1
  196. package/.next/static/chunks/app/api/web-auth/route-1dac1b3ca8adb975.js +0 -1
  197. package/.next/static/chunks/app/api/workspaces/route-1dac1b3ca8adb975.js +0 -1
  198. package/.next/static/chunks/app/layout-eb62bcd99abe79f9.js +0 -1
  199. package/.next/static/chunks/app/login/page-d2308b48d013d1a6.js +0 -1
  200. package/.next/static/chunks/app/page-708cd3ee4cfe8af5.js +0 -1
  201. package/.next/static/chunks/framework-0f597d6e68ffafed.js +0 -1
  202. package/.next/static/chunks/main-app-1a92e20ec190a32b.js +0 -1
  203. package/.next/static/chunks/next/dist/client/components/builtin/app-error-1dac1b3ca8adb975.js +0 -1
  204. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-1dac1b3ca8adb975.js +0 -1
  205. package/.next/static/chunks/next/dist/client/components/builtin/global-error-7bb5c4afcbc1772b.js +0 -1
  206. package/.next/static/chunks/next/dist/client/components/builtin/not-found-1dac1b3ca8adb975.js +0 -1
  207. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-1dac1b3ca8adb975.js +0 -1
  208. package/.next/static/chunks/webpack-9d84b382d4dec27a.js +0 -1
  209. /package/.next/static/{222KLUIvefiGf7M0Ov-Qz → olV7jxKRN5VhfEK012WhV}/_ssgManifest.js +0 -0
@@ -0,0 +1,333 @@
1
+ "use client";
2
+
3
+ /**
4
+ * 设置 → 导入会话:把本机其它 AI 编程工具的对话无损导成 pi 会话。
5
+ *
6
+ * 交互上刻意保守:**默认一条都不勾**(本机 ZCode 421 条、opencode 860 条,一键全选就是事故),
7
+ * 已导过的行标出来并且不能再勾。写入去向由服务端决定(源项目还在就导回原工作区),
8
+ * 这里只负责在源项目已不在本机时给一个兜底工作区——从已知工作区里选,不允许手输路径。
9
+ */
10
+ import { useCallback, useEffect, useMemo, useState } from "react";
11
+ import { useI18n } from "@/i18n";
12
+ import { IconRefreshOutline14, IconDownloadOutline16 } from "@/components/icons";
13
+
14
+ interface Summary {
15
+ source: string;
16
+ externalId: string;
17
+ title?: string;
18
+ projectPath?: string;
19
+ provider?: string;
20
+ model?: string;
21
+ createdAt?: number;
22
+ updatedAt?: number;
23
+ messageCount?: number;
24
+ }
25
+
26
+ interface SourceError {
27
+ source: string;
28
+ message: string;
29
+ }
30
+
31
+ interface ImportOutcome {
32
+ source: string;
33
+ externalId: string;
34
+ title?: string;
35
+ status: "imported" | "skipped" | "failed";
36
+ reason?: string;
37
+ messageCount?: number;
38
+ workspace?: string;
39
+ }
40
+
41
+ interface ImportReport {
42
+ imported: number;
43
+ skipped: number;
44
+ failed: number;
45
+ results: ImportOutcome[];
46
+ workspaces: string[];
47
+ }
48
+
49
+ /** 来源中文名/英文名放在 i18n 里,这里只保留稳定的顺序与徽标色 */
50
+ const SOURCE_ORDER = ["claude", "codex", "grok", "zcode", "dsh", "opencode"] as const;
51
+
52
+ /** 与 src/lib/session-import/index.ts 的 SCAN_LIMIT_PER_SOURCE 保持一致(仅用于提示文案) */
53
+ const SCAN_LIMIT = 15;
54
+
55
+ function dayOf(ms?: number): string {
56
+ if (!ms) return "";
57
+ const d = new Date(ms);
58
+ if (Number.isNaN(d.getTime())) return "";
59
+ const pad = (value: number) => String(value).padStart(2, "0");
60
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
61
+ }
62
+
63
+ export function SessionImportSection({ cwd }: { cwd: string }) {
64
+ const { t } = useI18n();
65
+ const [summaries, setSummaries] = useState<Summary[]>([]);
66
+ const [errors, setErrors] = useState<SourceError[]>([]);
67
+ const [importedKeys, setImportedKeys] = useState<Set<string>>(new Set());
68
+ const [selected, setSelected] = useState<Set<string>>(new Set());
69
+ const [scanning, setScanning] = useState(false);
70
+ const [importing, setImporting] = useState(false);
71
+ const [report, setReport] = useState<ImportReport | null>(null);
72
+ const [failure, setFailure] = useState<string | null>(null);
73
+ const [workspaces, setWorkspaces] = useState<string[]>([]);
74
+ const [fallback, setFallback] = useState(cwd);
75
+
76
+ const scan = useCallback(async () => {
77
+ setScanning(true);
78
+ setFailure(null);
79
+ try {
80
+ const response = await fetch("/api/sessions/import");
81
+ const json = await response.json();
82
+ if (!json.success) {
83
+ setFailure(json.error || t.toastError);
84
+ return;
85
+ }
86
+ setSummaries(json.data.sessions ?? []);
87
+ setErrors(json.data.errors ?? []);
88
+ setImportedKeys(new Set<string>(json.data.imported ?? []));
89
+ } catch (error) {
90
+ setFailure(error instanceof Error ? error.message : String(error));
91
+ } finally {
92
+ setScanning(false);
93
+ }
94
+ }, [t]);
95
+
96
+ // 兜底工作区候选:已知工作区列表(只选不填),默认当前打开的工作区
97
+ useEffect(() => {
98
+ void (async () => {
99
+ try {
100
+ const json = await (await fetch("/api/workspaces")).json();
101
+ if (json.success) setWorkspaces(json.data.workspaces ?? []);
102
+ } catch {
103
+ // 拿不到候选就把当前工作区当唯一条目,不影响导入
104
+ }
105
+ })();
106
+ }, []);
107
+
108
+ useEffect(() => {
109
+ void scan();
110
+ }, [scan]);
111
+
112
+ const key = (summary: Summary) => `${summary.source}:${summary.externalId}`;
113
+
114
+ const groups = useMemo(() => {
115
+ const bySource = new Map<string, Summary[]>();
116
+ for (const summary of summaries) {
117
+ const list = bySource.get(summary.source);
118
+ if (list) list.push(summary);
119
+ else bySource.set(summary.source, [summary]);
120
+ }
121
+ return SOURCE_ORDER.filter((source) => bySource.has(source)).map((source) => {
122
+ 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 };
124
+ });
125
+ }, [summaries, importedKeys]);
126
+
127
+ const toggle = (summary: Summary) => {
128
+ if (importedKeys.has(key(summary))) return;
129
+ setSelected((previous) => {
130
+ const next = new Set(previous);
131
+ if (next.has(key(summary))) next.delete(key(summary));
132
+ else next.add(key(summary));
133
+ return next;
134
+ });
135
+ };
136
+
137
+ const selectGroup = (rows: Summary[], checked: boolean) => {
138
+ setSelected((previous) => {
139
+ const next = new Set(previous);
140
+ for (const row of rows) {
141
+ if (importedKeys.has(key(row))) continue;
142
+ if (checked) next.add(key(row));
143
+ else next.delete(key(row));
144
+ }
145
+ return next;
146
+ });
147
+ };
148
+
149
+ const run = async () => {
150
+ if (!selected.size) return;
151
+ const items = summaries.filter((summary) => selected.has(key(summary))).map((summary) => ({ source: summary.source, externalId: summary.externalId }));
152
+ setImporting(true);
153
+ setFailure(null);
154
+ setReport(null);
155
+ try {
156
+ const response = await fetch("/api/sessions/import", {
157
+ method: "POST",
158
+ headers: { "Content-Type": "application/json" },
159
+ body: JSON.stringify({ sessions: items, fallbackCwd: fallback || undefined }),
160
+ });
161
+ const json = await response.json();
162
+ if (!json.success) {
163
+ setFailure(json.error || t.toastError);
164
+ return;
165
+ }
166
+ setReport(json.data as ImportReport);
167
+ setSelected(new Set());
168
+ await scan(); // 重新扫描:已经导过的行马上变成"已导入"
169
+ } catch (error) {
170
+ setFailure(error instanceof Error ? error.message : String(error));
171
+ } finally {
172
+ setImporting(false);
173
+ }
174
+ };
175
+
176
+ const sourceLabel = (source: string) => {
177
+ const labels: Record<string, string> = {
178
+ claude: t.importSourceClaude,
179
+ codex: t.importSourceCodex,
180
+ grok: t.importSourceGrok,
181
+ zcode: t.importSourceZcode,
182
+ dsh: t.importSourceDsh,
183
+ opencode: t.importSourceOpencode,
184
+ };
185
+ return labels[source] ?? source;
186
+ };
187
+
188
+ const failures = report?.results.filter((result) => result.status === "failed") ?? [];
189
+ const skipped = report?.results.filter((result) => result.status === "skipped") ?? [];
190
+
191
+ return (
192
+ <div className="flex flex-col gap-4" data-testid="session-import">
193
+ <div style={{ fontSize: 12, color: "var(--dsw-label-caption)" }}>{t.importDesc}</div>
194
+ <div style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }} title={t.importLimitHintTooltip} data-testid="import-limit-hint">
195
+ {t.importLimitHint.replace("{n}", String(SCAN_LIMIT))}
196
+ </div>
197
+
198
+ {/* 工具条:扫描 / 选择 / 兜底工作区 / 导入 */}
199
+ <div className="flex flex-wrap items-center gap-2">
200
+ <button
201
+ className="flex items-center gap-1.5 rounded-lg px-2.5 py-1.5"
202
+ style={{ fontSize: 12, border: "0.5px solid var(--dsw-border-l2)" }}
203
+ onClick={() => void scan()}
204
+ disabled={scanning}
205
+ data-testid="import-rescan"
206
+ >
207
+ <IconRefreshOutline14 size={13} /> {scanning ? t.importScanning : t.importRescan}
208
+ </button>
209
+ <span style={{ fontSize: 12, color: "var(--dsw-label-caption)" }}>{t.importFallbackWorkspace}</span>
210
+ <select
211
+ className="rounded-lg px-2 py-1"
212
+ style={{ fontSize: 12, border: "0.5px solid var(--dsw-border-l2)", background: "transparent", maxWidth: 260 }}
213
+ value={fallback}
214
+ onChange={(event) => setFallback(event.target.value)}
215
+ aria-label={t.importFallbackWorkspace}
216
+ data-testid="import-fallback"
217
+ >
218
+ {(workspaces.includes(fallback) || !fallback ? workspaces : [fallback, ...workspaces]).map((workspace) => (
219
+ <option key={workspace} value={workspace}>
220
+ {workspace}
221
+ </option>
222
+ ))}
223
+ </select>
224
+ <div className="flex-1" />
225
+ <button
226
+ className="rounded-lg px-2.5 py-1.5"
227
+ style={{ fontSize: 12, color: "var(--dsw-label-tertiary)" }}
228
+ onClick={() => setSelected(new Set())}
229
+ disabled={!selected.size}
230
+ >
231
+ {t.importClearSelection}
232
+ </button>
233
+ <button
234
+ className="flex items-center gap-1.5 rounded-lg px-3 py-1.5"
235
+ style={{ fontSize: 12, background: selected.size && !importing ? "var(--dsw-accent)" : "var(--dsw-hover)", color: selected.size && !importing ? "#fff" : "var(--dsw-label-caption)" }}
236
+ onClick={() => void run()}
237
+ disabled={!selected.size || importing}
238
+ data-testid="import-run"
239
+ >
240
+ <IconDownloadOutline16 size={13} /> {importing ? t.importRunning : t.importRun.replace("{n}", String(selected.size))}
241
+ </button>
242
+ </div>
243
+
244
+ {failure && (
245
+ <div role="alert" className="rounded-lg px-3 py-2" style={{ fontSize: 12.5, background: "var(--dsw-hover)", color: "var(--dsw-danger)" }}>
246
+ {failure}
247
+ </div>
248
+ )}
249
+
250
+ {errors.map((error) => (
251
+ <div key={error.source} className="rounded-lg px-3 py-2" style={{ fontSize: 12, background: "var(--dsw-hover)", color: "var(--dsw-label-tertiary)" }}>
252
+ {t.importSourceError.replace("{source}", sourceLabel(error.source)).replace("{message}", error.message)}
253
+ </div>
254
+ ))}
255
+
256
+ {report && (
257
+ <div className="rounded-2xl px-4 py-3" style={{ border: "0.5px solid var(--dsw-border-l2)" }} data-testid="import-report">
258
+ <div style={{ fontSize: 13 }}>
259
+ {t.importReportSummary
260
+ .replace("{imported}", String(report.imported))
261
+ .replace("{skipped}", String(report.skipped))
262
+ .replace("{failed}", String(report.failed))}
263
+ </div>
264
+ {skipped.map((result) => (
265
+ <div key={`skip-${result.source}-${result.externalId}`} className="mt-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
266
+ {sourceLabel(result.source)} · {result.title || result.externalId} — {result.reason}
267
+ </div>
268
+ ))}
269
+ {failures.map((result) => (
270
+ <div key={`fail-${result.source}-${result.externalId}`} className="mt-1" style={{ fontSize: 11.5, color: "var(--dsw-danger)" }}>
271
+ {sourceLabel(result.source)} · {result.title || result.externalId} — {result.reason}
272
+ </div>
273
+ ))}
274
+ </div>
275
+ )}
276
+
277
+ {!scanning && !summaries.length && !errors.length && <div style={{ fontSize: 13, color: "var(--dsw-label-caption)" }}>{t.importEmpty}</div>}
278
+
279
+ {groups.map((group) => (
280
+ <div key={group.source} className="flex flex-col gap-2" data-testid={`import-group-${group.source}`}>
281
+ <div className="flex items-center gap-2">
282
+ <span className="rounded-full px-2 py-0.5" style={{ fontSize: 10.5, background: "var(--dsw-hover)", color: "var(--dsw-label-tertiary)" }}>
283
+ {sourceLabel(group.source)}
284
+ </span>
285
+ <span style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
286
+ {t.importGroupCount.replace("{n}", String(group.rows.length)).replace("{imported}", String(group.imported))}
287
+ </span>
288
+ <div className="flex-1" />
289
+ <button className="rounded-lg px-2 py-0.5" style={{ fontSize: 11.5, color: "var(--dsw-label-tertiary)" }} onClick={() => selectGroup(group.rows, true)}>
290
+ {t.importSelectAll}
291
+ </button>
292
+ <button className="rounded-lg px-2 py-0.5" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }} onClick={() => selectGroup(group.rows, false)}>
293
+ {t.importSelectNone}
294
+ </button>
295
+ </div>
296
+ <div className="flex flex-col gap-1.5">
297
+ {group.rows.map((row) => {
298
+ const rowKey = key(row);
299
+ const done = importedKeys.has(rowKey);
300
+ const checked = selected.has(rowKey);
301
+ return (
302
+ <label
303
+ key={rowKey}
304
+ className="flex cursor-pointer items-center gap-3 rounded-xl px-3 py-2"
305
+ style={{ border: `0.5px solid ${checked ? "var(--dsw-accent)" : "var(--dsw-border-l2)"}`, opacity: done ? 0.55 : 1 }}
306
+ >
307
+ <input type="checkbox" checked={checked} disabled={done} onChange={() => toggle(row)} />
308
+ <div className="min-w-0 flex-1">
309
+ <div className="truncate" style={{ fontSize: 12.5 }} title={row.title}>
310
+ {row.title || t.importUntitled}
311
+ </div>
312
+ <div className="truncate" style={{ fontSize: 11, color: "var(--dsw-label-caption)" }} title={row.projectPath}>
313
+ {row.projectPath || t.importNoProject}
314
+ </div>
315
+ </div>
316
+ {done && (
317
+ <span className="rounded-full px-2 py-0.5" style={{ fontSize: 10, background: "var(--dsw-hover)", color: "var(--dsw-label-caption)" }}>
318
+ {t.importAlready}
319
+ </span>
320
+ )}
321
+ <span style={{ fontSize: 11, color: "var(--dsw-label-caption)", whiteSpace: "nowrap" }}>
322
+ {typeof row.messageCount === "number" ? t.importMessages.replace("{n}", String(row.messageCount)) : "—"}
323
+ </span>
324
+ <span style={{ fontSize: 11, color: "var(--dsw-label-caption)", whiteSpace: "nowrap" }}>{dayOf(row.updatedAt ?? row.createdAt)}</span>
325
+ </label>
326
+ );
327
+ })}
328
+ </div>
329
+ </div>
330
+ ))}
331
+ </div>
332
+ );
333
+ }
@@ -31,6 +31,7 @@ import {
31
31
  type BuiltinProviderSetup,
32
32
  type CustomProviderSetup,
33
33
  } from "@/components/ProviderSetupModal";
34
+ import { SessionImportSection } from "@/components/SessionImportSection";
34
35
  import { useI18n } from "@/i18n";
35
36
  import { applyPebrelTheme, loadPebrelTheme, loadThemeMode, type PebrelTheme, type ThemeMode } from "@/lib/theme";
36
37
  import { modelDraftFromConfig, serializeProviderDraft, validateModelDrafts, type ModelDraft } from "@/lib/model-draft";
@@ -39,7 +40,7 @@ import { quotaWindowKind, supportsUsageProbe, type BalanceAmount, type BalanceDe
39
40
  import type { ProviderView } from "@/lib/models-service";
40
41
  import type { ToolPreset } from "@/lib/types";
41
42
 
42
- type Section = "general" | "models" | "tools" | "skills" | "plugins";
43
+ type Section = "general" | "models" | "tools" | "skills" | "plugins" | "import";
43
44
 
44
45
  export function SettingsPanel({
45
46
  open,
@@ -83,6 +84,7 @@ export function SettingsPanel({
83
84
  { id: "tools", label: t.toolsSection, icon: <IconAgentPresetOutline16 size={15} /> },
84
85
  { id: "plugins", label: t.setPlugins, icon: <IconPluginOutline16 size={15} /> },
85
86
  { id: "skills", label: t.setSkills, icon: <IconSkillOutline16 size={15} /> },
87
+ { id: "import", label: t.importSessions, icon: <IconDownloadOutline16 size={15} /> },
86
88
  ];
87
89
 
88
90
  return (
@@ -149,6 +151,7 @@ export function SettingsPanel({
149
151
  {section === "skills" && <SkillsSection cwd={cwd} onOpenFileContent={onOpenFileContent} />}
150
152
  {section === "tools" && <ToolsSection toolPreset={toolPreset} onToolPresetChange={onToolPresetChange} tools={tools} onSetTools={onSetTools} />}
151
153
  {section === "plugins" && <PluginsSection cwd={cwd} />}
154
+ {section === "import" && <SessionImportSection cwd={cwd} />}
152
155
  </div>
153
156
  </div>
154
157
  </div>
@@ -978,12 +981,12 @@ function ModelsSection() {
978
981
  return (
979
982
  <>
980
983
  <div className="flex items-center gap-2">
981
- {!usage && <span>{failed ? t.usageProbeFailed : busy ? t.queryingQuota : providerId === "xai" ? t.usageXaiDisclosure : t.usageReadOnlyDisclosure}</span>}
984
+ {!usage && <span>{failed ? t.usageProbeFailed : busy ? t.queryingQuota : t.usageReadOnlyDisclosure}</span>}
982
985
  {usage?.kind === "quota" && usage.planType && <span>{t.usagePlan} {titleCase(usage.planType)}</span>}
983
986
  {usage?.kind === "balance" && usage.primary && (
984
987
  <span>{usage.primaryKey === "used" ? t.usageBalanceUsed : t.usageQuotaRemaining} <span style={{ color: "var(--dsw-label-secondary)" }}>{fmtAmount(usage.primary)}</span></span>
985
988
  )}
986
- <button type="button" className="pw-chip" disabled={busy} title={providerId === "xai" ? t.usageXaiDisclosure : t.usageReadOnlyDisclosure} onClick={() => void fetchProviderUsage(providerId, true)}>
989
+ <button type="button" className="pw-chip" disabled={busy} title={t.usageReadOnlyDisclosure} onClick={() => void fetchProviderUsage(providerId, true)}>
987
990
  {busy ? t.usageProbing : usage ? t.usageRefresh : t.usageProbe}
988
991
  </button>
989
992
  </div>
@@ -1230,69 +1233,71 @@ function ModelsSection() {
1230
1233
  const editingThis = editing === p.id;
1231
1234
  return (
1232
1235
  <div key={p.id}>
1233
- <div className="flex items-center gap-3 rounded-2xl px-4 py-3" style={{ border: "0.5px solid var(--dsw-border-l2)" }}>
1234
- <ProviderBrand id={p.id} name={providerName(p.id)} size={34} />
1235
- <div className="min-w-0 flex-1">
1236
- <div className="truncate" style={{ fontSize: 14, fontWeight: 500 }}>{providerName(p.id)}</div>
1237
- <div className="truncate" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
1238
- {p.id}{p.baseUrl ? ` · ${p.baseUrl}` : ""}
1236
+ {/* 卡片:身份/操作行在上,额度显示放在同一个框内的第二段(分隔线区分) */}
1237
+ <div className="flex flex-col rounded-2xl px-4 py-3" style={{ border: "0.5px solid var(--dsw-border-l2)" }}>
1238
+ <div className="flex items-center gap-3">
1239
+ <ProviderBrand id={p.id} name={providerName(p.id)} size={34} />
1240
+ <div className="min-w-0 flex-1">
1241
+ <div className="truncate" style={{ fontSize: 14, fontWeight: 500 }}>{providerName(p.id)}</div>
1242
+ <div className="truncate" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
1243
+ {p.id}{p.baseUrl ? ` · ${p.baseUrl}` : ""}
1244
+ </div>
1239
1245
  </div>
1240
- </div>
1241
- {isCustom && (
1246
+ {isCustom && (
1247
+ <span
1248
+ className="rounded-md px-1.5 py-0.5"
1249
+ style={{ fontSize: 10.5, border: "0.5px solid var(--dsw-border-l3)", color: "var(--dsw-label-tertiary)" }}
1250
+ >
1251
+ {t.customBadge}
1252
+ </span>
1253
+ )}
1254
+ <span
1255
+ className="inline-block h-2 w-2 rounded-full"
1256
+ style={{ background: p.authReady ? "var(--dsw-success)" : "var(--dsw-danger)" }}
1257
+ title={p.authError || (p.authReady ? t.providerReady : t.authUnavailable)}
1258
+ />
1242
1259
  <span
1243
1260
  className="rounded-md px-1.5 py-0.5"
1244
1261
  style={{ fontSize: 10.5, border: "0.5px solid var(--dsw-border-l3)", color: "var(--dsw-label-tertiary)" }}
1245
1262
  >
1246
- {t.customBadge}
1263
+ {t.credentialSource.replace("{source}", credentialSource(p))}
1247
1264
  </span>
1248
- )}
1249
- <span
1250
- className="inline-block h-2 w-2 rounded-full"
1251
- style={{ background: p.authReady ? "var(--dsw-success)" : "var(--dsw-danger)" }}
1252
- title={p.authError || (p.authReady ? t.providerReady : t.authUnavailable)}
1253
- />
1254
- <span
1255
- className="rounded-md px-1.5 py-0.5"
1256
- style={{ fontSize: 10.5, border: "0.5px solid var(--dsw-border-l3)", color: "var(--dsw-label-tertiary)" }}
1257
- >
1258
- {t.credentialSource.replace("{source}", credentialSource(p))}
1259
- </span>
1260
- <button className="btn-outline" style={{ height: 28, padding: "0 12px", fontSize: 12 }} onClick={() => startEdit(p.id)}>
1261
- {t.edit}
1262
- </button>
1263
- {p.storedAuthType === "oauth" && (
1264
- <button
1265
- style={{ height: 28, padding: "0 12px", fontSize: 12, color: "var(--dsw-danger)" }}
1266
- onClick={async () => {
1267
- if (!window.confirm(t.confirmRemoveOAuth.replace("{name}", providerName(p.id)))) return;
1268
- await call({ action: "removeKey", providerId: p.id });
1269
- }}
1270
- >
1271
- {t.removeOAuth}
1265
+ <button className="btn-outline" style={{ height: 28, padding: "0 12px", fontSize: 12 }} onClick={() => startEdit(p.id)}>
1266
+ {t.edit}
1272
1267
  </button>
1273
- )}
1274
- {p.authTypes?.includes("oauth") && (
1275
- <button
1276
- className="btn-outline"
1277
- style={{ height: 28, padding: "0 12px", fontSize: 12 }}
1278
- onClick={() => {
1279
- setOauthDone(null);
1280
- void call({ action: "loginStart", providerId: p.id });
1281
- setOauthProvider(p.id);
1282
- }}
1283
- >
1284
- {t.oauthLogin}
1285
- </button>
1286
- )}
1287
- {customIds.has(p.id) && (
1288
- <button style={{ fontSize: 12.5, color: "var(--dsw-danger)" }} onClick={() => deleteCustom(p.id)}>
1289
- {t.delete}
1290
- </button>
1291
- )}
1268
+ {p.storedAuthType === "oauth" && (
1269
+ <button
1270
+ style={{ height: 28, padding: "0 12px", fontSize: 12, color: "var(--dsw-danger)" }}
1271
+ onClick={async () => {
1272
+ if (!window.confirm(t.confirmRemoveOAuth.replace("{name}", providerName(p.id)))) return;
1273
+ await call({ action: "removeKey", providerId: p.id });
1274
+ }}
1275
+ >
1276
+ {t.removeOAuth}
1277
+ </button>
1278
+ )}
1279
+ {p.authTypes?.includes("oauth") && (
1280
+ <button
1281
+ className="btn-outline"
1282
+ style={{ height: 28, padding: "0 12px", fontSize: 12 }}
1283
+ onClick={() => {
1284
+ setOauthDone(null);
1285
+ void call({ action: "loginStart", providerId: p.id });
1286
+ setOauthProvider(p.id);
1287
+ }}
1288
+ >
1289
+ {t.oauthLogin}
1290
+ </button>
1291
+ )}
1292
+ {customIds.has(p.id) && (
1293
+ <button style={{ fontSize: 12.5, color: "var(--dsw-danger)" }} onClick={() => deleteCustom(p.id)}>
1294
+ {t.delete}
1295
+ </button>
1296
+ )}
1292
1297
  </div>
1293
1298
 
1294
1299
  {p.id === "xai" && p.authReady && (
1295
- <div className="mt-1.5 flex items-center gap-2 px-4" style={{ fontSize: 11.5, color: "var(--dsw-label-tertiary)" }}>
1300
+ <div className="mt-3 flex items-center gap-2 pt-3" style={{ borderTop: "0.5px solid var(--dsw-border-l2)", fontSize: 11.5, color: "var(--dsw-label-tertiary)" }}>
1296
1301
  {xaiUsage ? (
1297
1302
  <span>{t.usageQuotaLabel} · {xaiUsage.model} · {t.usageRequests} {xaiUsage.requestRemaining.toLocaleString()}/{xaiUsage.requestLimit.toLocaleString()} · Tokens {fmtQuotaTokens(xaiUsage.tokenRemaining)}/{fmtQuotaTokens(xaiUsage.tokenLimit)}</span>
1298
1303
  ) : <span>{usageError ? t.usageProbeFailed : t.usageProbeDisclosure}</span>}
@@ -1303,10 +1308,11 @@ function ModelsSection() {
1303
1308
  )}
1304
1309
 
1305
1310
  {supportsUsageProbe(p.id) && p.id !== "xai" && p.authReady && (
1306
- <div className="mt-1.5 flex flex-col gap-1.5 px-4" style={{ fontSize: 11.5, color: "var(--dsw-label-tertiary)" }}>
1311
+ <div className="mt-3 flex flex-col gap-1.5 pt-3" style={{ borderTop: "0.5px solid var(--dsw-border-l2)", fontSize: 11.5, color: "var(--dsw-label-tertiary)" }}>
1307
1312
  {renderProviderUsage(p.id)}
1308
1313
  </div>
1309
1314
  )}
1315
+ </div>
1310
1316
 
1311
1317
  {editingThis && (
1312
1318
  <div className="mt-2 rounded-2xl p-4" style={{ border: "0.5px solid var(--dsw-border-l2)" }}>
package/src/i18n.tsx CHANGED
@@ -21,7 +21,6 @@ const zh = {
21
21
  copyFailed: "复制失败",
22
22
  editMessage: "编辑这条消息",
23
23
  sendEdit: "重新发送",
24
- editWhileRunning: "会话正在运行,先停止再编辑",
25
24
  editMessageFailed: "编辑重发失败",
26
25
  export: "导出",
27
26
  settings: "设置",
@@ -214,7 +213,6 @@ const zh = {
214
213
  usageQuotaRemaining: "额度剩余",
215
214
  queryingQuota: "正在查询额度…",
216
215
  usageReadOnlyDisclosure: "读取提供商的只读用量端点,不消耗额度。",
217
- usageXaiDisclosure: "会向 xAI 发出一次 1 token 的请求以读取限流头,会计入用量。",
218
216
  usageReset: "刷新时间",
219
217
  usageWindowWeekly: "周限",
220
218
  usageWindowMonthly: "月限",
@@ -286,6 +284,28 @@ const zh = {
286
284
  contextSystem: "系统提示词",
287
285
  contextMessages: "对话消息",
288
286
  contextTools: "工具",
287
+ promptPanel: "提示词来源",
288
+ promptRefresh: "刷新",
289
+ promptLoading: "加载中…",
290
+ promptColdHint: "会话尚未启动:先列出磁盘上的提示词文件;打开会话后才会出现「组装后的系统提示词」与工具定义。",
291
+ promptOriginProject: "项目",
292
+ promptOriginAgent: "个人",
293
+ promptOriginPackage: "包",
294
+ promptOriginExtension: "扩展",
295
+ promptKindSystem: "系统提示词文件",
296
+ promptKindAppend: "追加提示词",
297
+ promptKindAgents: "项目记忆(AGENTS.md)",
298
+ promptKindSkill: "技能提示词",
299
+ promptKindTemplate: "提示模板",
300
+ promptKindTool: "工具提示词",
301
+ promptKindAssembled: "系统提示词",
302
+ promptKindCompacted: "压缩摘要",
303
+ promptCompactedKey: "进入上下文的压缩摘要",
304
+ promptGuidelines: "提示词指南",
305
+ promptParameters: "参数定义",
306
+ promptAssembledKey: "当前生效的系统提示词",
307
+ promptToolBuiltin: "内置",
308
+ promptToolExtension: "扩展",
289
309
  contextSystemPrompt: "System Prompt",
290
310
  contextSystemTools: "System Tools",
291
311
  contextCustomTools: "Custom Tools",
@@ -354,6 +374,33 @@ const zh = {
354
374
  enterQueue: "排队:本轮结束后发送",
355
375
  enterSteer: "插话:当前工具跑完就送达",
356
376
  setSkills: "技能",
377
+ // 导入会话(其它 AI 编程工具的本地对话 → pi 会话)
378
+ importSessions: "导入会话",
379
+ importDesc: "把本机 Codex / Claude Code / Grok / ZCode / dsh / opencode 的对话无损导入成 pi 会话:文本、思考、工具调用与结果、时间戳、用量都保留。源项目目录还在本机就导回原工作区,否则落到下面选的兜底工作区。只读源数据,不修改源工具的任何文件。",
380
+ importRescan: "重新扫描",
381
+ importScanning: "扫描中…",
382
+ importSourceClaude: "Claude Code",
383
+ importSourceCodex: "Codex",
384
+ importSourceGrok: "Grok",
385
+ importSourceZcode: "ZCode",
386
+ importSourceDsh: "dsh",
387
+ importSourceOpencode: "opencode",
388
+ importSourceError: "{source} 扫描失败:{message}",
389
+ importGroupCount: "{n} 条 · 已导入 {imported}",
390
+ importLimitHint: "每个来源只列最近 {n} 条对话(更早的会话不扫描,省时省资源)",
391
+ importLimitHintTooltip: "看更早的会话:把 src/lib/session-import/index.ts 里的 SCAN_LIMIT_PER_SOURCE 调大",
392
+ importSelectAll: "全选本组",
393
+ importSelectNone: "清空本组",
394
+ importClearSelection: "清空选择",
395
+ importRun: "导入所选 ({n})",
396
+ importRunning: "导入中…",
397
+ importFallbackWorkspace: "兜底工作区",
398
+ importAlready: "已导入",
399
+ importUntitled: "(无标题)",
400
+ importNoProject: "(无项目目录)",
401
+ importMessages: "{n} 条消息",
402
+ importEmpty: "没有找到可导入的本地会话",
403
+ importReportSummary: "成功 {imported} · 跳过 {skipped} · 失败 {failed}",
357
404
  setPlugins: "插件",
358
405
  appearance: "外观",
359
406
  appearanceMode: "亮暗模式",
@@ -579,7 +626,6 @@ const en: Dict = {
579
626
  copyFailed: "Copy failed",
580
627
  editMessage: "Edit this message",
581
628
  sendEdit: "Send again",
582
- editWhileRunning: "This session is running; stop it before editing",
583
629
  editMessageFailed: "Edit and resend failed",
584
630
  export: "Export",
585
631
  settings: "Settings",
@@ -771,7 +817,6 @@ const en: Dict = {
771
817
  usageQuotaRemaining: "Remaining quota",
772
818
  queryingQuota: "Querying quota…",
773
819
  usageReadOnlyDisclosure: "Reads the provider's read-only usage endpoint; consumes no quota.",
774
- usageXaiDisclosure: "Sends one 1-token request to xAI to read the rate-limit headers; counts against your usage.",
775
820
  usageReset: "Resets",
776
821
  usageWindowWeekly: "Weekly",
777
822
  usageWindowMonthly: "Monthly",
@@ -841,6 +886,28 @@ const en: Dict = {
841
886
  contextSystem: "System prompt",
842
887
  contextMessages: "Messages",
843
888
  contextTools: "Tools",
889
+ promptPanel: "Prompt sources",
890
+ promptRefresh: "Refresh",
891
+ promptLoading: "Loading…",
892
+ promptColdHint: "Session not started yet: disk files are listed now; the assembled system prompt and tool definitions appear once the session is running.",
893
+ promptOriginProject: "project",
894
+ promptOriginAgent: "user",
895
+ promptOriginPackage: "package",
896
+ promptOriginExtension: "extension",
897
+ promptKindSystem: "System prompt file",
898
+ promptKindAppend: "Appended prompt",
899
+ promptKindAgents: "Project memory (AGENTS.md)",
900
+ promptKindSkill: "Skill prompts",
901
+ promptKindTemplate: "Prompt templates",
902
+ promptKindTool: "Tool prompts",
903
+ promptKindAssembled: "System prompt",
904
+ promptKindCompacted: "Compaction summary",
905
+ promptCompactedKey: "Compaction summary in context",
906
+ promptGuidelines: "Prompt guidelines",
907
+ promptParameters: "Parameter schema",
908
+ promptAssembledKey: "Effective system prompt",
909
+ promptToolBuiltin: "builtin",
910
+ promptToolExtension: "extension",
844
911
  contextSystemPrompt: "System Prompt",
845
912
  contextSystemTools: "System Tools",
846
913
  contextCustomTools: "Custom Tools",
@@ -905,6 +972,33 @@ const en: Dict = {
905
972
  enterQueue: "Queue: sent after this turn ends",
906
973
  enterSteer: "Steer: delivered after the current tool call",
907
974
  setSkills: "Skills",
975
+ // Session import (other AI coding tools' local conversations → pi sessions)
976
+ importSessions: "Import sessions",
977
+ importDesc: "Losslessly import conversations from Codex / Claude Code / Grok / ZCode / dsh / opencode on this machine into pi sessions: text, thinking, tool calls and results, timestamps and usage are all kept. If the original project directory still exists locally the session lands in that workspace; otherwise it falls back to the workspace selected below. Source data is only read, never modified.",
978
+ importRescan: "Rescan",
979
+ importScanning: "Scanning…",
980
+ importSourceClaude: "Claude Code",
981
+ importSourceCodex: "Codex",
982
+ importSourceGrok: "Grok",
983
+ importSourceZcode: "ZCode",
984
+ importSourceDsh: "dsh",
985
+ importSourceOpencode: "opencode",
986
+ importSourceError: "{source} scan failed: {message}",
987
+ importGroupCount: "{n} sessions · {imported} imported",
988
+ importLimitHint: "Only the {n} most recent conversations per source are scanned",
989
+ importLimitHintTooltip: "To see older sessions, raise SCAN_LIMIT_PER_SOURCE in src/lib/session-import/index.ts",
990
+ importSelectAll: "Select group",
991
+ importSelectNone: "Clear group",
992
+ importClearSelection: "Clear",
993
+ importRun: "Import selected ({n})",
994
+ importRunning: "Importing…",
995
+ importFallbackWorkspace: "Fallback workspace",
996
+ importAlready: "Imported",
997
+ importUntitled: "(untitled)",
998
+ importNoProject: "(no project directory)",
999
+ importMessages: "{n} messages",
1000
+ importEmpty: "No importable local sessions found",
1001
+ importReportSummary: "Imported {imported} · skipped {skipped} · failed {failed}",
908
1002
  setPlugins: "Plugins",
909
1003
  appearance: "Appearance",
910
1004
  appearanceMode: "Light / Dark",