@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
@@ -684,17 +684,15 @@ async function buildContextBreakdown(
684
684
  // Memory:AGENTS.md 等注入文件(contextResources 的 project 部分)
685
685
  const memoryText = resources.filter((r) => r.source === "project").map((r) => r.content).join("\n\n");
686
686
  breakdown.memory = estimateTokensOf(memoryText);
687
- // Skills:从资源加载器取技能清单,按 system-prompt.js 的 formatSkillsForPrompt 段落定位
687
+ // Skills:从资源加载器取技能清单,按 SDK 的 formatSkillsForPrompt 段落定位。
688
+ // 该段落没有 Markdown 标题(原文以 "The following skills provide specialized instructions…" 开头,
689
+ // 结尾是 </available_skills>),以前按 "## Skills" 找永远找不到,技能段一直是 0。
688
690
  try {
689
691
  const { skills } = session.resourceLoader.getSkills();
690
692
  if (skills.length && prompt) {
691
- const header = prompt.indexOf("## Skills");
692
- if (header >= 0) {
693
- // skills 段之后是工具列表段(## Available Tools 等);找不到就取到结尾
694
- const next = prompt.slice(header + 1).search(/^## /m);
695
- const section = next >= 0 ? prompt.slice(header, header + 1 + next) : prompt.slice(header);
696
- breakdown.skills = estimateTokensOf(section);
697
- }
693
+ const start = prompt.indexOf("The following skills provide specialized instructions");
694
+ const end = prompt.indexOf("</available_skills>");
695
+ if (start >= 0 && end > start) breakdown.skills = estimateTokensOf(prompt.slice(start, end + "</available_skills>".length));
698
696
  }
699
697
  } catch {
700
698
  /* loader 不可用则跳过 */
@@ -1442,6 +1440,35 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
1442
1440
  const r = await session.navigateTree(entryId);
1443
1441
  return { ok: !r?.cancelled, data: { cancelled: r?.cancelled === true, editorText: r?.editorText } };
1444
1442
  }
1443
+ case "editAndResend": {
1444
+ // 编辑用户消息并重发:先停在跑的回合(用户改口即表示不要这一轮的输出),
1445
+ // 再回到该消息之前用新文本重新提问。放在服务端串行做,避免客户端"先 abort 再发"的竞态。
1446
+ const entryId = String((cmd as any).entryId ?? "");
1447
+ const text = (cmd.text ?? "").trim();
1448
+ if (!entryId) return { ok: false, error: "missing entryId" };
1449
+ if (!text) return { ok: false, error: "empty prompt" };
1450
+ if (m.promptSubmitting || m.resourceReloading) return { ok: false, error: "session is busy accepting another command" };
1451
+ m.promptSubmitting = true;
1452
+ try {
1453
+ const session = await ensureSession(m);
1454
+ if (m.disposed) throw new Error("session is disposed");
1455
+ if (session.isStreaming || m.runActive) {
1456
+ await session.abort().catch(() => undefined);
1457
+ // 等 SDK 真正回到空闲(abort 是异步的;最多等 5 秒,超时就按当前状态继续)
1458
+ for (let i = 0; i < 50 && !m.disposed && (session.isStreaming || m.runActive); i += 1) {
1459
+ await new Promise((resolve) => setTimeout(resolve, 100));
1460
+ }
1461
+ }
1462
+ if (m.disposed) throw new Error("session is disposed");
1463
+ const moved = await session.navigateTree(entryId);
1464
+ if (moved?.cancelled) return { ok: false, error: "edit cancelled" };
1465
+ await growthOf(m).prepare().catch(() => undefined);
1466
+ await session.prompt(text);
1467
+ return { ok: true };
1468
+ } finally {
1469
+ m.promptSubmitting = false;
1470
+ }
1471
+ }
1445
1472
  case "rename": {
1446
1473
  const name = (cmd.text ?? "").trim();
1447
1474
  if (m.session) m.session.setSessionName(name);
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 剪贴板工具:优先用异步 Clipboard API,非安全上下文(局域网 http)退回 textarea + execCommand。
3
+ * 从 ChatWindow 抽出来共用(消息操作行与提示词面板都用它)。
4
+ */
5
+ export async function copyText(text: string): Promise<boolean> {
6
+ try {
7
+ if (navigator.clipboard?.writeText) {
8
+ await navigator.clipboard.writeText(text);
9
+ return true;
10
+ }
11
+ } catch {
12
+ // Non-secure local-network contexts need the legacy fallback below.
13
+ }
14
+ const field = document.createElement("textarea");
15
+ field.value = text;
16
+ field.style.position = "fixed";
17
+ field.style.opacity = "0";
18
+ document.body.appendChild(field);
19
+ field.select();
20
+ try {
21
+ return document.execCommand("copy");
22
+ } catch {
23
+ return false;
24
+ } finally {
25
+ field.remove();
26
+ }
27
+ }
@@ -15,6 +15,7 @@ const COMMANDS = new Set<AgentCommand["cmd"]>([
15
15
  "fork",
16
16
  "cycleModel",
17
17
  "navigate",
18
+ "editAndResend",
18
19
  "clearQueue",
19
20
  "extensionUiResponse",
20
21
  "reload",
@@ -0,0 +1,229 @@
1
+ /**
2
+ * prompt-sources:集中列出所有承载提示词的来源,供界面上"点开即看"。
3
+ *
4
+ * 覆盖:系统提示词覆盖文件(SYSTEM.md)、追加提示词(APPEND_SYSTEM.md)、项目记忆(AGENTS.md)、
5
+ * 技能提示词(SKILL.md)、提示模板(prompts/*.md)、以及来自扩展与 npm 包的同类资源;
6
+ * 热会话还额外给出"组装后真正发给模型的系统提示词"和每个工具的定义(名称/描述/参数 schema)。
7
+ *
8
+ * 只读:不改动任何提示词内容,也不影响发给模型的东西。
9
+ */
10
+ import fs from "node:fs/promises";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import { getAgentDir, getResourceLoader, resourceLoaderReady } from "./pi";
14
+ import { resolveDiscoveredPath } from "./path-security";
15
+
16
+ /** 提示词来源类别:界面按这个分组 */
17
+ export type PromptSourceKind = "system" | "append" | "agents" | "skill" | "template" | "tool" | "assembled";
18
+
19
+ /** 来源出处:界面据此显示徽标(项目 / 个人 / 某个包 / 某个扩展) */
20
+ export interface PromptSourceOrigin {
21
+ scope: "project" | "agent" | "package" | "extension";
22
+ /** scope 为 package/extension 时的名字 */
23
+ name?: string;
24
+ }
25
+
26
+ export interface PromptSource {
27
+ kind: PromptSourceKind;
28
+ /** 展示名:文件名(不含目录)或工具名 */
29
+ name: string;
30
+ /** 绝对路径;合成项(assembled)没有路径 */
31
+ path?: string;
32
+ origin: PromptSourceOrigin;
33
+ /** 文件字节数(有路径且能 stat 到时) */
34
+ bytes?: number;
35
+ }
36
+
37
+ export interface PromptToolView {
38
+ name: string;
39
+ description?: string;
40
+ /** 参数 JSON schema,原样透出由界面格式化 */
41
+ parameters?: unknown;
42
+ promptGuidelines?: string[];
43
+ source: "builtin" | "extension";
44
+ }
45
+
46
+ export interface PromptSourcesResult {
47
+ sources: PromptSource[];
48
+ /** 热会话才有:实际发给模型的完整系统提示词(含你的 AGENTS.md、技能段、工具清单与追加段——不做任何过滤) */
49
+ assembledSystemPrompt?: string;
50
+ /** 热会话才有:压缩摘要(compact 后进入上下文的历史摘要,平时在界面上看不见) */
51
+ compactedSummary?: string;
52
+ /** 热会话才有:每个工具的定义 */
53
+ tools?: PromptToolView[];
54
+ /** 冷会话(agent 未启动)时为 false,界面据此说明为什么缺少合成项 */
55
+ sessionReady: boolean;
56
+ }
57
+
58
+ /** 只看这几个字段:够用且便于用假对象测试 */
59
+ export interface PromptSessionLike {
60
+ systemPrompt: string;
61
+ getAllTools(): Array<{
62
+ name: string;
63
+ description?: string;
64
+ parameters?: unknown;
65
+ promptGuidelines?: string[];
66
+ sourceInfo?: { source?: string; origin?: string; scope?: string };
67
+ }>;
68
+ /** 会话条目入口:SDK 里字段名有 sm / sessionManager 两种(压缩摘要从这里取) */
69
+ sm?: { getEntries?: () => unknown[] };
70
+ sessionManager?: { getEntries?: () => unknown[] };
71
+ }
72
+
73
+ interface SourceInfoLike {
74
+ source?: string;
75
+ scope?: string;
76
+ origin?: string;
77
+ }
78
+
79
+ const normalize = (value: string) => value.replace(/\\/g, "/").toLowerCase();
80
+
81
+ /** 从 node_modules 路径里取包名(含 scope) */
82
+ function packageNameFromPath(filePath: string): string | undefined {
83
+ const parts = filePath.replace(/\\/g, "/").split("/");
84
+ const index = parts.lastIndexOf("node_modules");
85
+ if (index < 0 || index + 1 >= parts.length) return undefined;
86
+ const first = parts[index + 1];
87
+ return first.startsWith("@") ? `${first}/${parts[index + 2] ?? ""}`.replace(/\/$/, "") : first;
88
+ }
89
+
90
+ /** 扩展目录名(<任意>/extensions/<name>/...) */
91
+ function extensionNameFromPath(filePath: string): string | undefined {
92
+ const parts = filePath.replace(/\\/g, "/").split("/");
93
+ const index = parts.lastIndexOf("extensions");
94
+ if (index < 0 || index + 1 >= parts.length) return undefined;
95
+ return parts[index + 1] || undefined;
96
+ }
97
+
98
+ /**
99
+ * 判断一个提示词文件来自哪里。优先用 SDK 的 SourceInfo(有 scope/origin),
100
+ * 没有时按路径归类:工作区内 = project,个人目录 = agent,node_modules = 某个包,
101
+ * extensions 目录 = 某个扩展。
102
+ */
103
+ export function classifyPromptOrigin(filePath: string, cwd: string, sourceInfo?: SourceInfoLike): PromptSourceOrigin {
104
+ const normalized = normalize(filePath);
105
+ const packageName = packageNameFromPath(filePath);
106
+ if (packageName || sourceInfo?.origin === "package") {
107
+ return { scope: "package", ...(packageName ?? sourceInfo?.source ? { name: packageName ?? sourceInfo?.source } : {}) };
108
+ }
109
+ const resolvedCwd = normalize(path.resolve(cwd));
110
+ if (normalized === resolvedCwd || normalized.startsWith(`${resolvedCwd}/`)) return { scope: "project" };
111
+ const agentDir = normalize(getAgentDir());
112
+ if (normalized.startsWith(`${agentDir}/`)) {
113
+ // 个人目录里的扩展也算扩展,便于区分"我自己装的那个扩展"
114
+ const extension = extensionNameFromPath(filePath);
115
+ return extension ? { scope: "extension", name: extension } : { scope: "agent" };
116
+ }
117
+ const extension = extensionNameFromPath(filePath);
118
+ if (extension) return { scope: "extension", name: extension };
119
+ if (sourceInfo?.scope === "project") return { scope: "project" };
120
+ return { scope: "agent" };
121
+ }
122
+
123
+ const KIND_ORDER: PromptSourceKind[] = ["system", "append", "agents", "skill", "template", "tool", "assembled"];
124
+
125
+ /** 纯函数:把各来源整理成界面要的列表(按类别、再按名字排序) */
126
+ export function buildPromptSources(entries: Array<Omit<PromptSource, "origin"> & { origin?: PromptSourceOrigin; cwd: string; sourceInfo?: SourceInfoLike }>): PromptSource[] {
127
+ return entries
128
+ .map(({ cwd, sourceInfo, origin, ...rest }) => ({
129
+ ...rest,
130
+ origin: origin ?? classifyPromptOrigin(rest.path ?? "", cwd, sourceInfo),
131
+ }))
132
+ .sort((a, b) => {
133
+ const byKind = KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind);
134
+ return byKind !== 0 ? byKind : a.name.localeCompare(b.name);
135
+ });
136
+ }
137
+
138
+ async function fileSize(filePath: string | undefined): Promise<number | undefined> {
139
+ if (!filePath) return undefined;
140
+ try {
141
+ const stat = await fs.stat(filePath);
142
+ return stat.isFile() ? stat.size : undefined;
143
+ } catch {
144
+ return undefined;
145
+ }
146
+ }
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
+
168
+ /** 列出当前工作区的全部提示词来源(冷会话也能列全文件类来源) */
169
+ export async function listPromptSources({
170
+ cwd,
171
+ session,
172
+ }: {
173
+ cwd: string;
174
+ session?: PromptSessionLike | null;
175
+ }): Promise<PromptSourcesResult> {
176
+ const resolvedCwd = path.resolve(cwd);
177
+ await resourceLoaderReady(resolvedCwd);
178
+ const loader = getResourceLoader(resolvedCwd);
179
+
180
+ const raw: Array<Omit<PromptSource, "origin"> & { cwd: string; sourceInfo?: SourceInfoLike; baseName?: string }> = [];
181
+ const push = (kind: PromptSourceKind, filePath: string | undefined, name: string | undefined, sourceInfo?: SourceInfoLike) => {
182
+ if (!filePath) return;
183
+ raw.push({ kind, path: filePath, name: name || path.basename(filePath), cwd: resolvedCwd, ...(sourceInfo ? { sourceInfo } : {}) });
184
+ };
185
+
186
+ const systemSource = loader.getSystemPromptSource();
187
+ push("system", systemSource?.path, systemSource ? path.basename(systemSource.path) : undefined);
188
+
189
+ for (const source of loader.getAppendSystemPromptSources()) push("append", source.path, source ? path.basename(source.path) : undefined);
190
+ for (const file of loader.getAgentsFiles().agentsFiles) push("agents", file.path, path.basename(file.path));
191
+ for (const skill of loader.getSkills().skills) push("skill", skill.filePath, skill.name || path.basename(skill.filePath), skill.sourceInfo as SourceInfoLike | undefined);
192
+ for (const template of loader.getPrompts().prompts) push("template", template.filePath, template.name || path.basename(template.filePath), template.sourceInfo as SourceInfoLike | undefined);
193
+
194
+ const withSizes = await Promise.all(raw.map(async (entry) => ({ ...entry, bytes: await fileSize(entry.path) })));
195
+ const sources = buildPromptSources(withSizes);
196
+ // 压缩摘要:compact 之后它以摘要形式进入上下文,界面上平时看不到,这里一并列出
197
+ const compacted = session ? latestCompactionSummary(session) : undefined;
198
+
199
+ const tools = session
200
+ ? session.getAllTools().map((tool) => ({
201
+ name: tool.name,
202
+ ...(tool.description === undefined ? {} : { description: tool.description }),
203
+ ...(tool.parameters === undefined ? {} : { parameters: tool.parameters }),
204
+ ...(tool.promptGuidelines?.length ? { promptGuidelines: tool.promptGuidelines } : {}),
205
+ source: tool.sourceInfo?.source === "builtin" ? ("builtin" as const) : ("extension" as const),
206
+ }))
207
+ : undefined;
208
+
209
+ return {
210
+ sources,
211
+ ...(session?.systemPrompt ? { assembledSystemPrompt: session.systemPrompt } : {}),
212
+ ...(compacted ? { compactedSummary: compacted } : {}),
213
+ ...(tools ? { tools } : {}),
214
+ sessionReady: Boolean(session),
215
+ };
216
+ }
217
+
218
+ /** 读取单个来源的内容:只允许读取"本次发现列表里出现过"的路径(与技能读取同一套校验) */
219
+ export async function readPromptSource(filePath: string, cwd: string): Promise<string> {
220
+ const { sources } = await listPromptSources({ cwd });
221
+ const allowed = sources.map((source) => source.path).filter((value): value is string => typeof value === "string");
222
+ const authorized = await resolveDiscoveredPath(filePath, allowed);
223
+ return fs.readFile(authorized, "utf8");
224
+ }
225
+
226
+ /** 给测试与诊断用:个人目录(提示词文件可能在 ~/.pi/agent 或 ~/.agents 下) */
227
+ export function promptHomeDirs(): string[] {
228
+ return [getAgentDir(), path.join(os.homedir(), ".agents")];
229
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Claude Code(~/.claude/projects/<slug>/*.jsonl)
3
+ *
4
+ * 每行自带 type,无统一头行;消息行是 {type:user|assistant, message:{role,model,content[],usage,stop_reason}}。
5
+ * content 块有 text / thinking / tool_use / tool_result / image;工具结果出现在后续 user 行里,
6
+ * 靠 tool_use_id 与前面的 tool_use 配对(这一步交给 writer 兜底修复)。
7
+ */
8
+ import path from "node:path";
9
+ import fs from "node:fs/promises";
10
+ import { eachJsonLine, listFiles, readHead, reachedLimit, sortByRecency } from "./files";
11
+ import { claudeRoot } from "./paths";
12
+ import type { ExternalSessionSummary, ImportedBlock, ImportedEntry, ImportedMessage, ImportedSession, ImportSourceModule, ScanOptions } from "./types";
13
+ import { cleanTitle, compactBlocks, outputBlocks, textBlock, toEpochMs } from "./types";
14
+
15
+ const ROOT = claudeRoot;
16
+
17
+ /** 合成信封(命令输出、系统提醒等)不是真人输入,导入时跳过 */
18
+ const SYNTHETIC_PREFIXES = ["<command-name>", "<command-message>", "<local-command-stdout>", "<local-command-caveat>", "<system-reminder>", "<user_info>", "<bash-input>", "<bash-stdout>"];
19
+
20
+ const isSynthetic = (text: string) => SYNTHETIC_PREFIXES.some((prefix) => text.startsWith(prefix));
21
+
22
+ function isMessageLine(record: Record<string, unknown>): boolean {
23
+ return (record.type === "user" || record.type === "assistant") && record.isSidechain !== true && Boolean(record.message);
24
+ }
25
+
26
+ function projectPathFrom(records: Record<string, unknown>[]): string | undefined {
27
+ for (const record of records) {
28
+ if (typeof record.cwd === "string" && record.cwd.trim()) return record.cwd;
29
+ }
30
+ return undefined;
31
+ }
32
+
33
+ function modelFrom(records: Record<string, unknown>[]): string | undefined {
34
+ for (const record of records) {
35
+ const message = record.message as { model?: unknown } | undefined;
36
+ if (typeof message?.model === "string" && message.model.trim()) return message.model;
37
+ }
38
+ return undefined;
39
+ }
40
+
41
+ /** 标题:优先会话摘要行,其次首条真人用户消息(Claude Code 不写标题字段) */
42
+ function titleFrom(records: Record<string, unknown>[]): string | undefined {
43
+ for (const record of records) {
44
+ if (record.type === "summary" && typeof record.summary === "string" && record.summary.trim()) return record.summary.trim();
45
+ }
46
+ for (const record of records) {
47
+ if (record.type !== "user") continue;
48
+ const plain = userTextOf((record.message as { content?: unknown } | undefined)?.content);
49
+ if (plain && !isSynthetic(plain)) return plain;
50
+ }
51
+ return undefined;
52
+ }
53
+
54
+ function userTextOf(content: unknown): string {
55
+ const items = Array.isArray(content) ? content : [content];
56
+ return items
57
+ .map((item) => (typeof item === "string" ? item : typeof (item as { text?: unknown })?.text === "string" ? ((item as { text: string }).text) : ""))
58
+ .join("\n")
59
+ .replace(/\s+/g, " ")
60
+ .trim();
61
+ }
62
+
63
+ /** assistant 消息的 content 数组 */
64
+ function assistantBlocks(content: unknown): ImportedBlock[] {
65
+ if (!Array.isArray(content)) return compactBlocks([textBlock(typeof content === "string" ? content : undefined)]);
66
+ return compactBlocks(content.flatMap((raw): Array<ImportedBlock | null> => {
67
+ const block = raw as Record<string, unknown>;
68
+ if (block?.type === "text") return [textBlock(block.text)];
69
+ if (block?.type === "thinking") return [typeof block.thinking === "string" && block.thinking ? { type: "thinking", thinking: block.thinking } : null];
70
+ if (block?.type === "tool_use") {
71
+ const id = typeof block.id === "string" ? block.id : "";
72
+ const name = typeof block.name === "string" ? block.name : "";
73
+ return id && name ? [{ type: "toolCall" as const, id, name, arguments: block.input ?? {} }] : [];
74
+ }
75
+ if (block?.type === "image") {
76
+ const source = block.source as Record<string, unknown> | undefined;
77
+ if (source?.type === "base64" && typeof source.data === "string" && typeof source.media_type === "string") {
78
+ return [{ type: "image", data: source.data, mimeType: source.media_type }];
79
+ }
80
+ }
81
+ return [];
82
+ }));
83
+ }
84
+
85
+ /** user 行可能同时含文本与工具结果:文本进用户消息,工具结果单列 */
86
+ function splitUserContent(content: unknown): { text: ImportedBlock[]; results: Array<{ toolCallId: string; content: ImportedBlock[]; isError: boolean }> } {
87
+ const text: Array<ImportedBlock | null> = [];
88
+ const results: Array<{ toolCallId: string; content: ImportedBlock[]; isError: boolean }> = [];
89
+ const items = Array.isArray(content) ? content : [content];
90
+ for (const raw of items) {
91
+ if (typeof raw === "string") {
92
+ text.push(textBlock(raw));
93
+ continue;
94
+ }
95
+ const block = raw as Record<string, unknown>;
96
+ if (block?.type === "text") text.push(textBlock(block.text));
97
+ else if (block?.type === "tool_result" && typeof block.tool_use_id === "string") {
98
+ results.push({ toolCallId: block.tool_use_id, content: outputBlocks(block.content), isError: block.is_error === true });
99
+ } else if (block?.type === "image") {
100
+ const source = block.source as Record<string, unknown> | undefined;
101
+ if (source?.type === "base64" && typeof source.data === "string" && typeof source.media_type === "string") {
102
+ text.push({ type: "image", data: source.data, mimeType: source.media_type });
103
+ }
104
+ }
105
+ }
106
+ return { text: compactBlocks(text), results };
107
+ }
108
+
109
+ export const claudeSource: ImportSourceModule = {
110
+ id: "claude",
111
+
112
+ async scan(options?: ScanOptions): Promise<ExternalSessionSummary[]> {
113
+ const limit = options?.limit ?? 0;
114
+ const candidates = await sortByRecency(await listFiles(ROOT(), { match: (name) => name.endsWith(".jsonl") }));
115
+ const out: ExternalSessionSummary[] = [];
116
+ for (const { file, mtime } of candidates) {
117
+ if (reachedLimit(out.length, limit)) break; // 只读最近的若干条:后面的文件根本不打开
118
+ let head: string;
119
+ try {
120
+ head = await readHead(file);
121
+ } catch {
122
+ continue;
123
+ }
124
+ const records = [...eachJsonLine(head)].filter((record) => record.type !== undefined).slice(0, 200);
125
+ const first = records.find(isMessageLine);
126
+ if (!first) continue; // 只有元信息、没有消息的文件不列
127
+ out.push({
128
+ source: "claude",
129
+ externalId: path.basename(file, ".jsonl"),
130
+ title: cleanTitle(titleFrom(records)),
131
+ projectPath: projectPathFrom(records),
132
+ model: modelFrom(records),
133
+ createdAt: toEpochMs(first.timestamp) ?? mtime,
134
+ updatedAt: mtime,
135
+ // 消息数需要整读,扫描阶段不统计(界面显示 —)
136
+ location: file,
137
+ });
138
+ }
139
+ return out;
140
+ },
141
+
142
+ async read(summary: ExternalSessionSummary): Promise<ImportedSession | null> {
143
+ let text: string;
144
+ try {
145
+ text = await fs.readFile(summary.location, "utf8");
146
+ } catch {
147
+ return null;
148
+ }
149
+ const entries: ImportedEntry[] = [];
150
+ const skipped: string[] = [];
151
+ let sidechains = 0;
152
+ let synthetic = 0;
153
+ let projectPath = summary.projectPath;
154
+ let model = summary.model;
155
+ let provider: string | undefined;
156
+
157
+ for (const record of eachJsonLine(text)) {
158
+ if (record.isSidechain === true) {
159
+ sidechains += 1;
160
+ continue;
161
+ }
162
+ if (typeof record.cwd === "string" && record.cwd.trim()) projectPath = projectPath ?? record.cwd;
163
+ const message = record.message as Record<string, unknown> | undefined;
164
+ if (!message) continue;
165
+ if (typeof message.model === "string" && message.model.trim()) model = message.model;
166
+ if (typeof message.provider === "string" && message.provider.trim()) provider = message.provider;
167
+ const timestamp = toEpochMs(record.timestamp) ?? toEpochMs(message.timestamp) ?? summary.createdAt ?? Date.now();
168
+
169
+ if (record.type === "assistant") {
170
+ const content = assistantBlocks(message.content);
171
+ if (!content.length) continue;
172
+ const usage = message.usage as Record<string, unknown> | undefined;
173
+ entries.push({
174
+ type: "message",
175
+ message: {
176
+ role: "assistant",
177
+ content,
178
+ timestamp,
179
+ ...(typeof message.model === "string" ? { model: message.model } : {}),
180
+ ...(typeof message.stop_reason === "string" && message.stop_reason ? { stopReason: message.stop_reason } : {}),
181
+ ...(usage
182
+ ? {
183
+ usage: {
184
+ input: Number(usage.input_tokens) || 0,
185
+ output: Number(usage.output_tokens) || 0,
186
+ cacheRead: Number(usage.cache_read_input_tokens) || 0,
187
+ cacheWrite: Number(usage.cache_creation_input_tokens) || 0,
188
+ },
189
+ }
190
+ : {}),
191
+ },
192
+ });
193
+ continue;
194
+ }
195
+
196
+ if (record.type === "user") {
197
+ const { text: textBlocks, results } = splitUserContent((message as { content?: unknown }).content);
198
+ const plain = textBlocks.filter((block) => block.type === "text").map((block) => block.text).join("\n");
199
+ const keepText = textBlocks.filter((block) => block.type !== "text");
200
+ if (plain && !isSynthetic(plain.trim()) && !/^Caveat:/i.test(plain.trim())) {
201
+ entries.push({ type: "message", message: { role: "user", content: [...keepText, { type: "text", text: plain }], timestamp } as ImportedMessage });
202
+ } else if (plain) {
203
+ synthetic += 1;
204
+ }
205
+ for (const result of results) {
206
+ entries.push({
207
+ type: "message",
208
+ message: { role: "toolResult", toolCallId: result.toolCallId, content: result.content, isError: result.isError || undefined, timestamp } as ImportedMessage,
209
+ });
210
+ }
211
+ }
212
+ }
213
+
214
+ if (sidechains) skipped.push(`跳过 ${sidechains} 条侧链(子代理)记录`);
215
+ if (synthetic) skipped.push(`跳过 ${synthetic} 条合成输入(命令回显/系统提醒)`);
216
+ if (!entries.some((entry) => entry.type === "message")) return null;
217
+ return { summary: { ...summary, projectPath, model, provider }, entries, skipped };
218
+ },
219
+ };