@rexvane/piweb 0.3.12 → 0.3.14

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 (177) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +8 -7
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/react-loadable-manifest.json +2 -2
  6. package/.next/routes-manifest.json +6 -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 -1
  42. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  43. package/.next/server/app/api/security/route.js +1 -1
  44. package/.next/server/app/api/security/route.js.nft.json +1 -1
  45. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  46. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  47. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  48. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  49. package/.next/server/app/api/sessions/import/route.js +6 -0
  50. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -0
  51. package/.next/server/app/api/sessions/import/route_client-reference-manifest.js +1 -0
  52. package/.next/server/app/api/sessions/route.js +1 -1
  53. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  54. package/.next/server/app/api/skills/route.js +2 -2
  55. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  56. package/.next/server/app/api/update/route.js +2 -2
  57. package/.next/server/app/api/update/route.js.nft.json +1 -1
  58. package/.next/server/app/api/version/route.js +1 -1
  59. package/.next/server/app/api/web-auth/route.js +1 -1
  60. package/.next/server/app/api/workspaces/route.js +1 -1
  61. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  62. package/.next/server/app/icon.svg/route.js +1 -1
  63. package/.next/server/app/index.html +1 -1
  64. package/.next/server/app/index.rsc +3 -3
  65. package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
  66. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  67. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  68. package/.next/server/app/login/page.js +1 -1
  69. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  70. package/.next/server/app/login.html +1 -1
  71. package/.next/server/app/login.rsc +2 -2
  72. package/.next/server/app/login.segments/_full.segment.rsc +2 -2
  73. package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
  74. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  75. package/.next/server/app/page.js +1 -1
  76. package/.next/server/app/page_client-reference-manifest.js +1 -1
  77. package/.next/server/app-paths-manifest.json +8 -7
  78. package/.next/server/chunks/496.js +1 -1
  79. package/.next/server/chunks/532.js +1 -0
  80. package/.next/server/chunks/686.js +2 -2
  81. package/.next/server/middleware-build-manifest.js +1 -1
  82. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  83. package/.next/server/pages/404.html +1 -1
  84. package/.next/server/pages/500.html +1 -1
  85. package/.next/server/server-reference-manifest.json +1 -1
  86. package/.next/static/LOFl9bqqMV2goBV8erESO/_buildManifest.js +1 -0
  87. package/.next/static/chunks/4632.112d2e893d48d188.js +1 -0
  88. package/.next/static/chunks/4686-7d694def920ff093.js +1 -0
  89. package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +1 -0
  90. package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +1 -0
  91. package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +1 -0
  92. package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +1 -0
  93. package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +1 -0
  94. package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +1 -0
  95. package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +1 -0
  96. package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +1 -0
  97. package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +1 -0
  98. package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +1 -0
  99. package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +1 -0
  100. package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +1 -0
  101. package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +1 -0
  102. package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +1 -0
  103. package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +1 -0
  104. package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +1 -0
  105. package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +1 -0
  106. package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +1 -0
  107. package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +1 -0
  108. package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +1 -0
  109. package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +1 -0
  110. package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +1 -0
  111. package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +1 -0
  112. package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +1 -0
  113. package/.next/static/chunks/app/page-9a5440047c5a415c.js +1 -0
  114. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +1 -0
  115. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +1 -0
  116. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +1 -0
  117. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +1 -0
  118. package/.next/static/chunks/{webpack-05a9e85529a18b93.js → webpack-d28e132e1b8c100c.js} +1 -1
  119. package/.next/types/app/api/sessions/import/route.ts +353 -0
  120. package/.next/types/routes.d.ts +2 -1
  121. package/.next/types/validator.ts +9 -0
  122. package/README.md +6 -2
  123. package/package.json +1 -1
  124. package/src/app/api/sessions/import/route.ts +74 -0
  125. package/src/components/PromptPanel.tsx +33 -31
  126. package/src/components/SessionImportSection.tsx +370 -0
  127. package/src/components/SettingsPanel.tsx +4 -1
  128. package/src/i18n.tsx +68 -6
  129. package/src/lib/prompt-sources.ts +29 -1
  130. package/src/lib/session-import/claude.ts +229 -0
  131. package/src/lib/session-import/codex.ts +223 -0
  132. package/src/lib/session-import/dsh-zstd.ts +69 -0
  133. package/src/lib/session-import/dsh.ts +259 -0
  134. package/src/lib/session-import/files.ts +84 -0
  135. package/src/lib/session-import/grok.ts +169 -0
  136. package/src/lib/session-import/index.ts +229 -0
  137. package/src/lib/session-import/oc-family.ts +275 -0
  138. package/src/lib/session-import/opencode.ts +7 -0
  139. package/src/lib/session-import/paths.ts +37 -0
  140. package/src/lib/session-import/sqlite.ts +89 -0
  141. package/src/lib/session-import/types.ts +171 -0
  142. package/src/lib/session-import/writer.ts +259 -0
  143. package/src/lib/session-import/zcode.ts +8 -0
  144. package/src/lib/skills-service.ts +51 -14
  145. package/.next/server/chunks/118.js +0 -1
  146. package/.next/static/2kt2UiWjv_viXpNlfjvBT/_buildManifest.js +0 -1
  147. package/.next/static/chunks/4127.dff5c3b33c6bfd1b.js +0 -1
  148. package/.next/static/chunks/4686-5bbee099126af797.js +0 -1
  149. package/.next/static/chunks/app/_global-error/page-961c055baebb3872.js +0 -1
  150. package/.next/static/chunks/app/api/agent/[id]/events/route-961c055baebb3872.js +0 -1
  151. package/.next/static/chunks/app/api/agent/[id]/route-961c055baebb3872.js +0 -1
  152. package/.next/static/chunks/app/api/agent/[id]/tree/route-961c055baebb3872.js +0 -1
  153. package/.next/static/chunks/app/api/agent/new/route-961c055baebb3872.js +0 -1
  154. package/.next/static/chunks/app/api/config/route-961c055baebb3872.js +0 -1
  155. package/.next/static/chunks/app/api/files/route-961c055baebb3872.js +0 -1
  156. package/.next/static/chunks/app/api/git/route-961c055baebb3872.js +0 -1
  157. package/.next/static/chunks/app/api/growth/route-961c055baebb3872.js +0 -1
  158. package/.next/static/chunks/app/api/health/route-961c055baebb3872.js +0 -1
  159. package/.next/static/chunks/app/api/models/route-961c055baebb3872.js +0 -1
  160. package/.next/static/chunks/app/api/pi-settings/route-961c055baebb3872.js +0 -1
  161. package/.next/static/chunks/app/api/plugins/route-961c055baebb3872.js +0 -1
  162. package/.next/static/chunks/app/api/prompts/route-961c055baebb3872.js +0 -1
  163. package/.next/static/chunks/app/api/security/route-961c055baebb3872.js +0 -1
  164. package/.next/static/chunks/app/api/sessions/[id]/export/route-961c055baebb3872.js +0 -1
  165. package/.next/static/chunks/app/api/sessions/[id]/route-961c055baebb3872.js +0 -1
  166. package/.next/static/chunks/app/api/sessions/route-961c055baebb3872.js +0 -1
  167. package/.next/static/chunks/app/api/skills/route-961c055baebb3872.js +0 -1
  168. package/.next/static/chunks/app/api/update/route-961c055baebb3872.js +0 -1
  169. package/.next/static/chunks/app/api/version/route-961c055baebb3872.js +0 -1
  170. package/.next/static/chunks/app/api/web-auth/route-961c055baebb3872.js +0 -1
  171. package/.next/static/chunks/app/api/workspaces/route-961c055baebb3872.js +0 -1
  172. package/.next/static/chunks/app/page-00c8ebf40ec5eaca.js +0 -1
  173. package/.next/static/chunks/next/dist/client/components/builtin/app-error-961c055baebb3872.js +0 -1
  174. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-961c055baebb3872.js +0 -1
  175. package/.next/static/chunks/next/dist/client/components/builtin/not-found-961c055baebb3872.js +0 -1
  176. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-961c055baebb3872.js +0 -1
  177. /package/.next/static/{2kt2UiWjv_viXpNlfjvBT → LOFl9bqqMV2goBV8erESO}/_ssgManifest.js +0 -0
@@ -0,0 +1,370 @@
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
+ /**
128
+ * 一次只显示一个来源:六个工具各列 15 条就是 90 行,全铺出来既难找也没必要。
129
+ * 默认落在第一个有会话的来源上(SOURCE_ORDER 的顺序),用户切换后不被扫描结果重置。
130
+ */
131
+ 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);
137
+
138
+ const toggle = (summary: Summary) => {
139
+ if (importedKeys.has(key(summary))) return;
140
+ setSelected((previous) => {
141
+ const next = new Set(previous);
142
+ if (next.has(key(summary))) next.delete(key(summary));
143
+ else next.add(key(summary));
144
+ return next;
145
+ });
146
+ };
147
+
148
+ const selectGroup = (rows: Summary[], checked: boolean) => {
149
+ setSelected((previous) => {
150
+ const next = new Set(previous);
151
+ 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));
155
+ }
156
+ return next;
157
+ });
158
+ };
159
+
160
+ const run = async () => {
161
+ if (!selected.size) return;
162
+ const items = summaries.filter((summary) => selected.has(key(summary))).map((summary) => ({ source: summary.source, externalId: summary.externalId }));
163
+ setImporting(true);
164
+ setFailure(null);
165
+ setReport(null);
166
+ try {
167
+ const response = await fetch("/api/sessions/import", {
168
+ method: "POST",
169
+ headers: { "Content-Type": "application/json" },
170
+ body: JSON.stringify({ sessions: items, fallbackCwd: fallback || undefined }),
171
+ });
172
+ const json = await response.json();
173
+ if (!json.success) {
174
+ setFailure(json.error || t.toastError);
175
+ return;
176
+ }
177
+ setReport(json.data as ImportReport);
178
+ setSelected(new Set());
179
+ await scan(); // 重新扫描:已经导过的行马上变成"已导入"
180
+ } catch (error) {
181
+ setFailure(error instanceof Error ? error.message : String(error));
182
+ } finally {
183
+ setImporting(false);
184
+ }
185
+ };
186
+
187
+ const sourceLabel = (source: string) => {
188
+ const labels: Record<string, string> = {
189
+ claude: t.importSourceClaude,
190
+ codex: t.importSourceCodex,
191
+ grok: t.importSourceGrok,
192
+ zcode: t.importSourceZcode,
193
+ dsh: t.importSourceDsh,
194
+ opencode: t.importSourceOpencode,
195
+ };
196
+ return labels[source] ?? source;
197
+ };
198
+
199
+ const failures = report?.results.filter((result) => result.status === "failed") ?? [];
200
+ const skipped = report?.results.filter((result) => result.status === "skipped") ?? [];
201
+
202
+ return (
203
+ <div className="flex flex-col gap-4" data-testid="session-import">
204
+ <div style={{ fontSize: 12, color: "var(--dsw-label-caption)" }}>{t.importDesc}</div>
205
+ <div style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }} title={t.importLimitHintTooltip} data-testid="import-limit-hint">
206
+ {t.importLimitHint.replace("{n}", String(SCAN_LIMIT))}
207
+ </div>
208
+
209
+ {/* 工具条:扫描 / 选择 / 兜底工作区 / 导入 */}
210
+ <div className="flex flex-wrap items-center gap-2">
211
+ <button
212
+ className="flex items-center gap-1.5 rounded-lg px-2.5 py-1.5"
213
+ style={{ fontSize: 12, border: "0.5px solid var(--dsw-border-l2)" }}
214
+ onClick={() => void scan()}
215
+ disabled={scanning}
216
+ data-testid="import-rescan"
217
+ >
218
+ <IconRefreshOutline14 size={13} /> {scanning ? t.importScanning : t.importRescan}
219
+ </button>
220
+ <span style={{ fontSize: 12, color: "var(--dsw-label-caption)" }}>{t.importFallbackWorkspace}</span>
221
+ <select
222
+ className="rounded-lg px-2 py-1"
223
+ style={{ fontSize: 12, border: "0.5px solid var(--dsw-border-l2)", background: "transparent", maxWidth: 260 }}
224
+ value={fallback}
225
+ onChange={(event) => setFallback(event.target.value)}
226
+ aria-label={t.importFallbackWorkspace}
227
+ data-testid="import-fallback"
228
+ >
229
+ {(workspaces.includes(fallback) || !fallback ? workspaces : [fallback, ...workspaces]).map((workspace) => (
230
+ <option key={workspace} value={workspace}>
231
+ {workspace}
232
+ </option>
233
+ ))}
234
+ </select>
235
+ <div className="flex-1" />
236
+ <button
237
+ className="rounded-lg px-2.5 py-1.5"
238
+ style={{ fontSize: 12, color: "var(--dsw-label-tertiary)" }}
239
+ onClick={() => setSelected(new Set())}
240
+ disabled={!selected.size}
241
+ >
242
+ {t.importClearSelection}
243
+ </button>
244
+ <button
245
+ className="flex items-center gap-1.5 rounded-lg px-3 py-1.5"
246
+ style={{ fontSize: 12, background: selected.size && !importing ? "var(--dsw-accent)" : "var(--dsw-hover)", color: selected.size && !importing ? "#fff" : "var(--dsw-label-caption)" }}
247
+ onClick={() => void run()}
248
+ disabled={!selected.size || importing}
249
+ data-testid="import-run"
250
+ >
251
+ <IconDownloadOutline16 size={13} /> {importing ? t.importRunning : t.importRun.replace("{n}", String(selected.size))}
252
+ </button>
253
+ </div>
254
+
255
+ {failure && (
256
+ <div role="alert" className="rounded-lg px-3 py-2" style={{ fontSize: 12.5, background: "var(--dsw-hover)", color: "var(--dsw-danger)" }}>
257
+ {failure}
258
+ </div>
259
+ )}
260
+
261
+ {errors.map((error) => (
262
+ <div key={error.source} className="rounded-lg px-3 py-2" style={{ fontSize: 12, background: "var(--dsw-hover)", color: "var(--dsw-label-tertiary)" }}>
263
+ {t.importSourceError.replace("{source}", sourceLabel(error.source)).replace("{message}", error.message)}
264
+ </div>
265
+ ))}
266
+
267
+ {report && (
268
+ <div className="rounded-2xl px-4 py-3" style={{ border: "0.5px solid var(--dsw-border-l2)" }} data-testid="import-report">
269
+ <div style={{ fontSize: 13 }}>
270
+ {t.importReportSummary
271
+ .replace("{imported}", String(report.imported))
272
+ .replace("{skipped}", String(report.skipped))
273
+ .replace("{failed}", String(report.failed))}
274
+ </div>
275
+ {skipped.map((result) => (
276
+ <div key={`skip-${result.source}-${result.externalId}`} className="mt-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
277
+ {sourceLabel(result.source)} · {result.title || result.externalId} — {result.reason}
278
+ </div>
279
+ ))}
280
+ {failures.map((result) => (
281
+ <div key={`fail-${result.source}-${result.externalId}`} className="mt-1" style={{ fontSize: 11.5, color: "var(--dsw-danger)" }}>
282
+ {sourceLabel(result.source)} · {result.title || result.externalId} — {result.reason}
283
+ </div>
284
+ ))}
285
+ </div>
286
+ )}
287
+
288
+ {!scanning && !summaries.length && !errors.length && <div style={{ fontSize: 13, color: "var(--dsw-label-caption)" }}>{t.importEmpty}</div>}
289
+
290
+ {/* 来源选择器:选中哪个就只列哪个工具的会话 */}
291
+ {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">
293
+ {groups.map((group) => {
294
+ const active = group.source === activeSource;
295
+ return (
296
+ <button
297
+ key={group.source}
298
+ type="button"
299
+ role="tab"
300
+ 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
+ }}
308
+ onClick={() => setActiveSource(group.source)}
309
+ data-testid={`import-source-${group.source}`}
310
+ >
311
+ {sourceLabel(group.source)}
312
+ <span style={{ marginLeft: 6, opacity: 0.75 }}>{group.rows.length}</span>
313
+ </button>
314
+ );
315
+ })}
316
+ </div>
317
+ )}
318
+
319
+ {activeGroup && (
320
+ <div className="flex flex-col gap-2" data-testid={`import-group-${activeGroup.source}`}>
321
+ <div className="flex items-center gap-2">
322
+ <span style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
323
+ {t.importGroupCount.replace("{n}", String(activeGroup.rows.length)).replace("{imported}", String(activeGroup.imported))}
324
+ </span>
325
+ <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)}>
327
+ {t.importSelectAll}
328
+ </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)}>
330
+ {t.importSelectNone}
331
+ </button>
332
+ </div>
333
+ <div className="flex flex-col gap-1.5">
334
+ {activeGroup.rows.map((row) => {
335
+ const rowKey = key(row);
336
+ const done = importedKeys.has(rowKey);
337
+ const checked = selected.has(rowKey);
338
+ return (
339
+ <label
340
+ 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 }}
343
+ >
344
+ <input type="checkbox" checked={checked} disabled={done} onChange={() => toggle(row)} />
345
+ <div className="min-w-0 flex-1">
346
+ <div className="truncate" style={{ fontSize: 12.5 }} title={row.title}>
347
+ {row.title || t.importUntitled}
348
+ </div>
349
+ <div className="truncate" style={{ fontSize: 11, color: "var(--dsw-label-caption)" }} title={row.projectPath}>
350
+ {row.projectPath || t.importNoProject}
351
+ </div>
352
+ </div>
353
+ {done && (
354
+ <span className="rounded-full px-2 py-0.5" style={{ fontSize: 10, background: "var(--dsw-hover)", color: "var(--dsw-label-caption)" }}>
355
+ {t.importAlready}
356
+ </span>
357
+ )}
358
+ <span style={{ fontSize: 11, color: "var(--dsw-label-caption)", whiteSpace: "nowrap" }}>
359
+ {typeof row.messageCount === "number" ? t.importMessages.replace("{n}", String(row.messageCount)) : "—"}
360
+ </span>
361
+ <span style={{ fontSize: 11, color: "var(--dsw-label-caption)", whiteSpace: "nowrap" }}>{dayOf(row.updatedAt ?? row.createdAt)}</span>
362
+ </label>
363
+ );
364
+ })}
365
+ </div>
366
+ </div>
367
+ )}
368
+ </div>
369
+ );
370
+ }
@@ -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>
package/src/i18n.tsx CHANGED
@@ -298,11 +298,14 @@ const zh = {
298
298
  promptKindSkill: "技能提示词",
299
299
  promptKindTemplate: "提示模板",
300
300
  promptKindTool: "工具提示词",
301
- promptKindAssembled: "组装后的系统提示词",
302
- promptAssembledKey: "当前生效的完整系统提示词",
301
+ promptKindAssembled: "系统提示词",
302
+ promptKindCompacted: "压缩摘要",
303
+ promptCompactedKey: "进入上下文的压缩摘要",
304
+ promptGuidelines: "提示词指南",
305
+ promptParameters: "参数定义",
306
+ promptAssembledKey: "当前生效的系统提示词",
303
307
  promptToolBuiltin: "内置",
304
308
  promptToolExtension: "扩展",
305
- copySuccess: "已复制",
306
309
  contextSystemPrompt: "System Prompt",
307
310
  contextSystemTools: "System Tools",
308
311
  contextCustomTools: "Custom Tools",
@@ -371,6 +374,34 @@ const zh = {
371
374
  enterQueue: "排队:本轮结束后发送",
372
375
  enterSteer: "插话:当前工具跑完就送达",
373
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
+ importPickSource: "选择来源",
391
+ importLimitHint: "每个来源只列最近 {n} 条对话(更早的会话不扫描,省时省资源)",
392
+ importLimitHintTooltip: "看更早的会话:把 src/lib/session-import/index.ts 里的 SCAN_LIMIT_PER_SOURCE 调大",
393
+ importSelectAll: "全选本组",
394
+ importSelectNone: "清空本组",
395
+ importClearSelection: "清空选择",
396
+ importRun: "导入所选 ({n})",
397
+ importRunning: "导入中…",
398
+ importFallbackWorkspace: "兜底工作区",
399
+ importAlready: "已导入",
400
+ importUntitled: "(无标题)",
401
+ importNoProject: "(无项目目录)",
402
+ importMessages: "{n} 条消息",
403
+ importEmpty: "没有找到可导入的本地会话",
404
+ importReportSummary: "成功 {imported} · 跳过 {skipped} · 失败 {failed}",
374
405
  setPlugins: "插件",
375
406
  appearance: "外观",
376
407
  appearanceMode: "亮暗模式",
@@ -870,11 +901,14 @@ const en: Dict = {
870
901
  promptKindSkill: "Skill prompts",
871
902
  promptKindTemplate: "Prompt templates",
872
903
  promptKindTool: "Tool prompts",
873
- promptKindAssembled: "Assembled system prompt",
874
- promptAssembledKey: "Effective system prompt for this session",
904
+ promptKindAssembled: "System prompt",
905
+ promptKindCompacted: "Compaction summary",
906
+ promptCompactedKey: "Compaction summary in context",
907
+ promptGuidelines: "Prompt guidelines",
908
+ promptParameters: "Parameter schema",
909
+ promptAssembledKey: "Effective system prompt",
875
910
  promptToolBuiltin: "builtin",
876
911
  promptToolExtension: "extension",
877
- copySuccess: "Copied",
878
912
  contextSystemPrompt: "System Prompt",
879
913
  contextSystemTools: "System Tools",
880
914
  contextCustomTools: "Custom Tools",
@@ -939,6 +973,34 @@ const en: Dict = {
939
973
  enterQueue: "Queue: sent after this turn ends",
940
974
  enterSteer: "Steer: delivered after the current tool call",
941
975
  setSkills: "Skills",
976
+ // Session import (other AI coding tools' local conversations → pi sessions)
977
+ importSessions: "Import sessions",
978
+ 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.",
979
+ importRescan: "Rescan",
980
+ importScanning: "Scanning…",
981
+ importSourceClaude: "Claude Code",
982
+ importSourceCodex: "Codex",
983
+ importSourceGrok: "Grok",
984
+ importSourceZcode: "ZCode",
985
+ importSourceDsh: "dsh",
986
+ importSourceOpencode: "opencode",
987
+ importSourceError: "{source} scan failed: {message}",
988
+ importGroupCount: "{n} sessions · {imported} imported",
989
+ importPickSource: "Choose a source",
990
+ importLimitHint: "Only the {n} most recent conversations per source are scanned",
991
+ importLimitHintTooltip: "To see older sessions, raise SCAN_LIMIT_PER_SOURCE in src/lib/session-import/index.ts",
992
+ importSelectAll: "Select group",
993
+ importSelectNone: "Clear group",
994
+ importClearSelection: "Clear",
995
+ importRun: "Import selected ({n})",
996
+ importRunning: "Importing…",
997
+ importFallbackWorkspace: "Fallback workspace",
998
+ importAlready: "Imported",
999
+ importUntitled: "(untitled)",
1000
+ importNoProject: "(no project directory)",
1001
+ importMessages: "{n} messages",
1002
+ importEmpty: "No importable local sessions found",
1003
+ importReportSummary: "Imported {imported} · skipped {skipped} · failed {failed}",
942
1004
  setPlugins: "Plugins",
943
1005
  appearance: "Appearance",
944
1006
  appearanceMode: "Light / Dark",
@@ -45,8 +45,10 @@ export interface PromptToolView {
45
45
 
46
46
  export interface PromptSourcesResult {
47
47
  sources: PromptSource[];
48
- /** 热会话才有:组装后发给模型的完整系统提示词 */
48
+ /** 热会话才有:实际发给模型的完整系统提示词(含你的 AGENTS.md、技能段、工具清单与追加段——不做任何过滤) */
49
49
  assembledSystemPrompt?: string;
50
+ /** 热会话才有:压缩摘要(compact 后进入上下文的历史摘要,平时在界面上看不见) */
51
+ compactedSummary?: string;
50
52
  /** 热会话才有:每个工具的定义 */
51
53
  tools?: PromptToolView[];
52
54
  /** 冷会话(agent 未启动)时为 false,界面据此说明为什么缺少合成项 */
@@ -63,6 +65,9 @@ export interface PromptSessionLike {
63
65
  promptGuidelines?: string[];
64
66
  sourceInfo?: { source?: string; origin?: string; scope?: string };
65
67
  }>;
68
+ /** 会话条目入口:SDK 里字段名有 sm / sessionManager 两种(压缩摘要从这里取) */
69
+ sm?: { getEntries?: () => unknown[] };
70
+ sessionManager?: { getEntries?: () => unknown[] };
66
71
  }
67
72
 
68
73
  interface SourceInfoLike {
@@ -140,6 +145,26 @@ async function fileSize(filePath: string | undefined): Promise<number | undefine
140
145
  }
141
146
  }
142
147
 
148
+ /** 最近一次压缩的摘要(与上下文计量同一套取法:从会话条目的最后一条 compaction 往前找) */
149
+ export function latestCompactionSummary(session: PromptSessionLike): string | undefined {
150
+ try {
151
+ const entries = sessionEntries(session);
152
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
153
+ const entry = entries[i] as { type?: string; summary?: string };
154
+ if (entry?.type === "compaction" && typeof entry.summary === "string" && entry.summary.trim()) return entry.summary;
155
+ }
156
+ } catch {
157
+ /* 条目不可用则跳过 */
158
+ }
159
+ return undefined;
160
+ }
161
+
162
+ /** 会话条目入口在 SDK 里有两个字段名(sm / sessionManager),两种都认 */
163
+ function sessionEntries(session: PromptSessionLike): unknown[] {
164
+ const s = session as unknown as { sm?: { getEntries?: () => unknown[] }; sessionManager?: { getEntries?: () => unknown[] } };
165
+ return s.sm?.getEntries?.() ?? s.sessionManager?.getEntries?.() ?? [];
166
+ }
167
+
143
168
  /** 列出当前工作区的全部提示词来源(冷会话也能列全文件类来源) */
144
169
  export async function listPromptSources({
145
170
  cwd,
@@ -168,6 +193,8 @@ export async function listPromptSources({
168
193
 
169
194
  const withSizes = await Promise.all(raw.map(async (entry) => ({ ...entry, bytes: await fileSize(entry.path) })));
170
195
  const sources = buildPromptSources(withSizes);
196
+ // 压缩摘要:compact 之后它以摘要形式进入上下文,界面上平时看不到,这里一并列出
197
+ const compacted = session ? latestCompactionSummary(session) : undefined;
171
198
 
172
199
  const tools = session
173
200
  ? session.getAllTools().map((tool) => ({
@@ -182,6 +209,7 @@ export async function listPromptSources({
182
209
  return {
183
210
  sources,
184
211
  ...(session?.systemPrompt ? { assembledSystemPrompt: session.systemPrompt } : {}),
212
+ ...(compacted ? { compactedSummary: compacted } : {}),
185
213
  ...(tools ? { tools } : {}),
186
214
  sessionReady: Boolean(session),
187
215
  };