@rexvane/piweb 0.3.18 → 0.3.19

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 (261) 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 +3 -3
  4. package/.next/prerender-manifest.json +4 -4
  5. package/.next/react-loadable-manifest.json +23 -9
  6. package/.next/required-server-files.js +2 -1
  7. package/.next/required-server-files.json +2 -1
  8. package/.next/routes-manifest.json +12 -0
  9. package/.next/server/app/_global-error/page.js +2 -2
  10. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_global-error.html +1 -1
  12. package/.next/server/app/_global-error.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page.js +1 -1
  17. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/_not-found.html +1 -1
  19. package/.next/server/app/_not-found.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/server/app/api/agent/[id]/events/route.js +3 -3
  24. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  25. package/.next/server/app/api/agent/[id]/route.js +1 -1
  26. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  27. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  28. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  29. package/.next/server/app/api/agent/new/route.js +1 -1
  30. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  31. package/.next/server/app/api/config/route.js +1 -1
  32. package/.next/server/app/api/config/route.js.nft.json +1 -1
  33. package/.next/server/app/api/files/route.js +1 -1
  34. package/.next/server/app/api/files/route.js.nft.json +1 -1
  35. package/.next/server/app/api/fs/route.js +1 -0
  36. package/.next/server/app/api/fs/route.js.nft.json +1 -0
  37. package/.next/server/app/api/fs/route_client-reference-manifest.js +1 -0
  38. package/.next/server/app/api/git/route.js +2 -2
  39. package/.next/server/app/api/git/route.js.nft.json +1 -1
  40. package/.next/server/app/api/growth/route.js +1 -1
  41. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  42. package/.next/server/app/api/health/route.js +1 -1
  43. package/.next/server/app/api/models/route.js +1 -1
  44. package/.next/server/app/api/models/route.js.nft.json +1 -1
  45. package/.next/server/app/api/pi-settings/route.js +1 -1
  46. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  47. package/.next/server/app/api/plugins/route.js +1 -1
  48. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  49. package/.next/server/app/api/prompts/route.js +1 -1
  50. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  51. package/.next/server/app/api/remote/route.js +8 -0
  52. package/.next/server/app/api/remote/route.js.nft.json +1 -0
  53. package/.next/server/app/api/remote/route_client-reference-manifest.js +1 -0
  54. package/.next/server/app/api/security/route.js +1 -1
  55. package/.next/server/app/api/security/route.js.nft.json +1 -1
  56. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  57. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  58. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  59. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  60. package/.next/server/app/api/sessions/import/route.js +2 -2
  61. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  62. package/.next/server/app/api/sessions/route.js +1 -1
  63. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  64. package/.next/server/app/api/skills/route.js +1 -1
  65. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  66. package/.next/server/app/api/update/route.js +2 -2
  67. package/.next/server/app/api/update/route.js.nft.json +1 -1
  68. package/.next/server/app/api/version/route.js +1 -1
  69. package/.next/server/app/api/version/route.js.nft.json +1 -1
  70. package/.next/server/app/api/web-auth/route.js +3 -1
  71. package/.next/server/app/api/workspaces/route.js +1 -1
  72. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  73. package/.next/server/app/icon.svg/route.js +1 -1
  74. package/.next/server/app/index.html +1 -1
  75. package/.next/server/app/index.rsc +4 -4
  76. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  77. package/.next/server/app/index.segments/_full.segment.rsc +4 -4
  78. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  79. package/.next/server/app/login/page.js +1 -1
  80. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  81. package/.next/server/app/login.html +1 -1
  82. package/.next/server/app/login.rsc +3 -3
  83. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  84. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  85. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -3
  86. package/.next/server/app/page.js +1 -1
  87. package/.next/server/app/page.js.nft.json +1 -1
  88. package/.next/server/app/page_client-reference-manifest.js +1 -1
  89. package/.next/server/app-paths-manifest.json +8 -6
  90. package/.next/server/chunks/229.js +1 -1
  91. package/.next/server/chunks/243.js +1 -0
  92. package/.next/server/chunks/393.js +1 -0
  93. package/.next/server/chunks/496.js +1 -1
  94. package/.next/server/chunks/538.js +1 -0
  95. package/.next/server/chunks/686.js +1 -4
  96. package/.next/server/chunks/802.js +2 -0
  97. package/.next/server/chunks/807.js +1 -0
  98. package/.next/server/chunks/920.js +4 -0
  99. package/.next/server/chunks/924.js +6 -0
  100. package/.next/server/instrumentation.js +1 -0
  101. package/.next/server/instrumentation.js.nft.json +1 -0
  102. package/.next/server/middleware-build-manifest.js +1 -1
  103. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  104. package/.next/server/middleware.js +4 -3
  105. package/.next/server/pages/404.html +1 -1
  106. package/.next/server/pages/500.html +1 -1
  107. package/.next/server/server-reference-manifest.json +1 -1
  108. package/.next/static/JDGSR-TIbk9qZWcUNYIX3/_buildManifest.js +1 -0
  109. package/.next/static/chunks/2716.7ad073038e94a802.js +1 -0
  110. package/.next/static/chunks/316-5ea443ee6b512aa9.js +1 -0
  111. package/.next/static/chunks/4673.8a5a9e6855350e34.js +1 -0
  112. package/.next/static/chunks/4686-e3825f3b6a384aa1.js +1 -0
  113. package/.next/static/chunks/4778.e4c9ed093a413bc4.js +1 -0
  114. package/.next/static/chunks/5173.91ddb803785009f8.js +1 -0
  115. package/.next/static/chunks/839.7403c6bdace4c56c.js +1 -0
  116. package/.next/static/chunks/8552.6bfe3e101250b20f.js +1 -0
  117. package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +1 -0
  118. package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +1 -0
  119. package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +1 -0
  120. package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +1 -0
  121. package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +1 -0
  122. package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +1 -0
  123. package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +1 -0
  124. package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +1 -0
  125. package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +1 -0
  126. package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +1 -0
  127. package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +1 -0
  128. package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +1 -0
  129. package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +1 -0
  130. package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +1 -0
  131. package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +1 -0
  132. package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +1 -0
  133. package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +1 -0
  134. package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +1 -0
  135. package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +1 -0
  136. package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +1 -0
  137. package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +1 -0
  138. package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +1 -0
  139. package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +1 -0
  140. package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +1 -0
  141. package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +1 -0
  142. package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +1 -0
  143. package/.next/static/chunks/app/login/page-5e276a1eeb8867df.js +1 -0
  144. package/.next/static/chunks/app/page-2e425ef83fba0782.js +1 -0
  145. package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +1 -0
  146. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +1 -0
  147. package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +1 -0
  148. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +1 -0
  149. package/.next/static/chunks/webpack-bd2b9202ecbcdcaa.js +1 -0
  150. package/.next/static/css/7391c42e1ae617f9.css +1 -0
  151. package/.next/types/app/api/fs/route.ts +353 -0
  152. package/.next/types/app/api/remote/route.ts +353 -0
  153. package/.next/types/routes.d.ts +3 -1
  154. package/.next/types/validator.ts +18 -0
  155. package/README.md +56 -10
  156. package/next.config.ts +3 -2
  157. package/package.json +3 -3
  158. package/scripts/launcher.mjs +194 -13
  159. package/scripts/release.mjs +0 -2
  160. package/src/app/api/agent/[id]/events/route.ts +11 -6
  161. package/src/app/api/fs/route.ts +119 -0
  162. package/src/app/api/growth/route.ts +11 -6
  163. package/src/app/api/models/route.ts +21 -9
  164. package/src/app/api/remote/route.ts +67 -0
  165. package/src/app/api/sessions/[id]/export/route.ts +11 -1
  166. package/src/app/api/sessions/route.ts +3 -1
  167. package/src/app/api/web-auth/route.ts +45 -49
  168. package/src/app/api/workspaces/route.ts +16 -0
  169. package/src/app/globals.css +6 -0
  170. package/src/app/login/page.tsx +8 -60
  171. package/src/components/AppShell.tsx +202 -155
  172. package/src/components/ChatInput.tsx +61 -6
  173. package/src/components/ChatWindow.tsx +274 -132
  174. package/src/components/DirectoryPicker.tsx +189 -0
  175. package/src/components/DrawerPanel.tsx +164 -0
  176. package/src/components/FileViewer.tsx +2 -2
  177. package/src/components/GrowthTree.tsx +6 -5
  178. package/src/components/MarkdownBlock.tsx +134 -0
  179. package/src/components/OutlineRail.tsx +22 -1
  180. package/src/components/ProjectPanel.tsx +197 -14
  181. package/src/components/RemoteSection.tsx +232 -0
  182. package/src/components/SessionImportSection.tsx +60 -38
  183. package/src/components/SettingsPanel.tsx +104 -57
  184. package/src/components/icons.tsx +23 -0
  185. package/src/hooks/useGrowth.ts +47 -25
  186. package/src/hooks/usePiWeb.ts +100 -14
  187. package/src/i18n.tsx +115 -28
  188. package/src/instrumentation-node.ts +35 -0
  189. package/src/instrumentation.ts +18 -0
  190. package/src/lib/agent-config.ts +134 -0
  191. package/src/lib/agent-manager.ts +243 -61
  192. package/src/lib/auth-throttle.ts +102 -0
  193. package/src/lib/auth.ts +35 -27
  194. package/src/lib/files-service.ts +61 -1
  195. package/src/lib/font.ts +84 -0
  196. package/src/lib/git-service.ts +5 -3
  197. package/src/lib/growth-service.ts +78 -30
  198. package/src/lib/growth-tracker.ts +91 -9
  199. package/src/lib/models-service.ts +36 -11
  200. package/src/lib/pi-settings.ts +10 -12
  201. package/src/lib/pi.ts +145 -18
  202. package/src/lib/plugins-service.ts +67 -35
  203. package/src/lib/remote/assets.json +4 -0
  204. package/src/lib/remote/mobile-proxy.ts +149 -0
  205. package/src/lib/remote/supervisor.ts +482 -0
  206. package/src/lib/security-service.ts +6 -11
  207. package/src/lib/session-import/claude.ts +45 -21
  208. package/src/lib/session-reader.ts +106 -3
  209. package/src/lib/skills-service.ts +34 -12
  210. package/src/lib/text-sanitize.ts +12 -0
  211. package/src/lib/theme.ts +3 -0
  212. package/src/lib/types.ts +16 -2
  213. package/src/lib/update-service.ts +0 -1
  214. package/src/lib/web-auth-shared.ts +5 -1
  215. package/src/lib/web-auth.ts +139 -22
  216. package/src/lib/web-launch-token.ts +55 -0
  217. package/src/lib/web-secret.ts +182 -0
  218. package/src/lib/workspace-store.ts +91 -7
  219. package/src/proxy.ts +46 -12
  220. package/.next/server/chunks/532.js +0 -1
  221. package/.next/static/chunks/2155.2f27fe7098987699.js +0 -1
  222. package/.next/static/chunks/2608.ec8ff1f5767c9056.js +0 -1
  223. package/.next/static/chunks/2716.93be8ad5c970c516.js +0 -1
  224. package/.next/static/chunks/4632.112d2e893d48d188.js +0 -1
  225. package/.next/static/chunks/4673.c1fb1580efc6ace7.js +0 -1
  226. package/.next/static/chunks/4686-f4a037d609cea3f2.js +0 -1
  227. package/.next/static/chunks/692-7992db0ae9cc3021.js +0 -1
  228. package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +0 -1
  229. package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +0 -1
  230. package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +0 -1
  231. package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +0 -1
  232. package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +0 -1
  233. package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +0 -1
  234. package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +0 -1
  235. package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +0 -1
  236. package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +0 -1
  237. package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +0 -1
  238. package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +0 -1
  239. package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +0 -1
  240. package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +0 -1
  241. package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +0 -1
  242. package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +0 -1
  243. package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +0 -1
  244. package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +0 -1
  245. package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +0 -1
  246. package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +0 -1
  247. package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +0 -1
  248. package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +0 -1
  249. package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +0 -1
  250. package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +0 -1
  251. package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +0 -1
  252. package/.next/static/chunks/app/login/page-52ef4841cbd199a8.js +0 -1
  253. package/.next/static/chunks/app/page-b51087831582ee37.js +0 -1
  254. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +0 -1
  255. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +0 -1
  256. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +0 -1
  257. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +0 -1
  258. package/.next/static/chunks/webpack-c31263fd83d75ce6.js +0 -1
  259. package/.next/static/css/0cd968b9b62035b5.css +0 -1
  260. package/.next/static/en03hnPiuro0rnf8QkFEe/_buildManifest.js +0 -1
  261. /package/.next/static/{en03hnPiuro0rnf8QkFEe → JDGSR-TIbk9qZWcUNYIX3}/_ssgManifest.js +0 -0
@@ -6,6 +6,7 @@
6
6
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
7
  import { createEventBatcher, type EventBatcher } from "@/lib/event-batcher";
8
8
  import type {
9
+ GrowthConsentRequest,
9
10
  GrowthRound,
10
11
  SessionSummary,
11
12
  TrajEntry,
@@ -61,7 +62,7 @@ export interface PiWebState {
61
62
  /** 扩展覆盖的「工作中」文案(setWorkingMessage) */
62
63
  workingMessage: string | null;
63
64
  /** 项目生长:本次连接期间收到的 commit(完整时间轴由 useGrowth 拉取后按 commit 去重合并) */
64
- growth: { rounds: GrowthRound[]; error: string | null };
65
+ growth: { rounds: GrowthRound[]; error: string | null; consent: GrowthConsentRequest | null };
65
66
  }
66
67
 
67
68
  export interface SessionListItem extends SessionSummary {
@@ -76,8 +77,10 @@ export const foldPiWebEvent = (state: PiWebState, evt: WebEvent): PiWebState =>
76
77
  case "delta": {
77
78
  // 新服务端按稳定流 ID 定位;旧协议仍按最近 pending 消息兼容。
78
79
  const msgs = [...s.messages];
79
- let i = evt.messageId ? msgs.findIndex((message) => message.streamId === evt.messageId || message.id === evt.messageId) : msgs.length - 1;
80
- if (!evt.messageId) while (i >= 0 && msgs[i].role !== "assistant" && msgs[i].role !== "user") i -= 1;
80
+ // 从尾部找:增量几乎总是落在最后一条消息上,长会话里从头扫每个 token 都是一次全表遍历
81
+ let i = msgs.length - 1;
82
+ if (evt.messageId) while (i >= 0 && msgs[i].streamId !== evt.messageId && msgs[i].id !== evt.messageId) i -= 1;
83
+ else while (i >= 0 && msgs[i].role !== "assistant" && msgs[i].role !== "user") i -= 1;
81
84
  if (evt.messageId && i >= 0 && msgs[i].stopReason && msgs[i].stopReason !== "pending") return s;
82
85
  if (i < 0 || msgs[i].role !== "assistant" || (msgs[i].stopReason ?? "pending") !== "pending") {
83
86
  msgs.push({ role: "assistant", content: [], streamId: evt.messageId, stopReason: "pending", timestamp: evt.ts });
@@ -234,12 +237,15 @@ export const foldPiWebEvent = (state: PiWebState, evt: WebEvent): PiWebState =>
234
237
  return s;
235
238
  case "growth": {
236
239
  const rounds = s.growth.rounds.some((x) => x.commit === evt.round.commit) ? s.growth.rounds : [...s.growth.rounds, evt.round];
237
- s.growth = { rounds, error: null };
240
+ s.growth = { ...s.growth, rounds, error: null };
238
241
  return s;
239
242
  }
240
243
  case "growth_error":
241
244
  s.growth = { ...s.growth, error: evt.message };
242
245
  return s;
246
+ case "growth_consent":
247
+ s.growth = { ...s.growth, consent: evt.request };
248
+ return s;
243
249
  case "retry":
244
250
  // 只保留最后一次尝试:重试过程在界面上是「重试 3/5」这一条,而不是五行报错
245
251
  s.retryNotice = evt.maxAttempts
@@ -315,7 +321,7 @@ const emptyState = (toolPreset: ToolPreset = "standard"): PiWebState => ({
315
321
  extensionNotices: [],
316
322
  extensionStatuses: {},
317
323
  workingMessage: null,
318
- growth: { rounds: [], error: null },
324
+ growth: { rounds: [], error: null, consent: null },
319
325
  });
320
326
 
321
327
  function pathKey(value: string): string {
@@ -337,6 +343,9 @@ export function usePiWeb() {
337
343
  const [models, setModels] = useState<{ providers: any[]; models: any[] } | null>(null);
338
344
  const [addedWorkspaces, setAddedWorkspaces] = useState<string[]>([]);
339
345
  const [removedWorkspaces, setRemovedWorkspaces] = useState<string[]>([]);
346
+ /** 应用内目录浏览器(远程设备专用):是否打开,以及等待用户选择的那次调用 */
347
+ const [dirPickerOpen, setDirPickerOpen] = useState(false);
348
+ const dirPickResolve = useRef<((path: string | null) => void) | null>(null);
340
349
  const [groupBy, setGroupByState] = useState<"workspace" | "flat">("workspace");
341
350
  const [orderBy, setOrderByState] = useState<"updated" | "manual">("updated");
342
351
  const [resyncNonce, setResyncNonce] = useState(0);
@@ -400,10 +409,15 @@ export function usePiWeb() {
400
409
  } catch (error) {
401
410
  const message = error instanceof Error ? error.message : "failed to load workspaces";
402
411
  setState((current) => ({ ...current, error: message }));
412
+ throw error;
403
413
  }
404
414
  }, []);
405
- useEffect(() => {
406
- refreshWorkspaces();
415
+ // 挂载时不再单独拉一次 /api/workspaces:首个 /api/sessions 响应里带着同一份 registry,
416
+ // 少一次请求也少一次服务端读盘;只有会话列表拉不到时才用它兜底(见轮询的 catch)。
417
+ const workspaceRegistryLoaded = useRef(false);
418
+ const refreshWorkspacesFallback = useCallback(async () => {
419
+ if (workspaceRegistryLoaded.current) return;
420
+ await refreshWorkspaces().catch(() => undefined);
407
421
  }, [refreshWorkspaces]);
408
422
 
409
423
  /** 会话改名成功后先把列表里的名字改掉,不等下一次轮询(轮询 3 秒 + 服务端 1.2 秒列表缓存) */
@@ -489,6 +503,14 @@ export function usePiWeb() {
489
503
  });
490
504
  const j = await r.json();
491
505
  if (!r.ok || !j.success) throw new Error(j.error || `request failed (${r.status})`);
506
+ // 远程设备(手机走 tailnet):原生对话框会弹在主机屏幕上,等于没反应。
507
+ // 服务端据此回 remote:true,这里改用应用内目录浏览器,等选完再返回路径。
508
+ if (j.data?.remote) {
509
+ setDirPickerOpen(true);
510
+ return await new Promise<string | null>((resolve) => {
511
+ dirPickResolve.current = resolve;
512
+ });
513
+ }
492
514
  if (j.success && j.data?.path) {
493
515
  setAddedWorkspaces(j.data.workspaces ?? []);
494
516
  await refreshWorkspaces();
@@ -502,6 +524,37 @@ export function usePiWeb() {
502
524
  }
503
525
  }, [refreshWorkspaces]);
504
526
 
527
+ /** 应用内目录浏览器:用户选定了某个文件夹 */
528
+ const confirmDirPicker = useCallback(async (path: string) => {
529
+ setDirPickerOpen(false);
530
+ const settle = dirPickResolve.current;
531
+ dirPickResolve.current = null;
532
+ try {
533
+ const r = await fetch("/api/workspaces", {
534
+ method: "POST",
535
+ headers: { "Content-Type": "application/json" },
536
+ body: JSON.stringify({ action: "add", path }),
537
+ });
538
+ const j = await r.json();
539
+ if (!r.ok || !j.success) throw new Error(j.error || `request failed (${r.status})`);
540
+ setAddedWorkspaces(j.data?.workspaces ?? []);
541
+ await refreshWorkspaces();
542
+ settle?.(path);
543
+ } catch (error) {
544
+ const message = error instanceof Error ? error.message : "failed to add workspace";
545
+ setState((current) => ({ ...current, error: message }));
546
+ settle?.(null);
547
+ }
548
+ }, [refreshWorkspaces]);
549
+
550
+ /** 应用内目录浏览器:取消 */
551
+ const cancelDirPicker = useCallback(() => {
552
+ setDirPickerOpen(false);
553
+ const settle = dirPickResolve.current;
554
+ dirPickResolve.current = null;
555
+ settle?.(null);
556
+ }, []);
557
+
505
558
 
506
559
  const removeWorkspace = useCallback(
507
560
  async (dir: string) => {
@@ -556,6 +609,7 @@ export function usePiWeb() {
556
609
  const running: Record<string, { streaming: boolean; lastStep?: string }> = j.data.running ?? {};
557
610
  const registry = j.data.workspaceRegistry;
558
611
  if (registry) {
612
+ workspaceRegistryLoaded.current = true;
559
613
  if (Array.isArray(registry.workspaces)) setAddedWorkspaces(registry.workspaces);
560
614
  if (Array.isArray(registry.removedWorkspaces)) setRemovedWorkspaces(registry.removedWorkspaces);
561
615
  if (registry.aliases && typeof registry.aliases === "object") setWorkspaceAliases(registry.aliases);
@@ -565,8 +619,11 @@ export function usePiWeb() {
565
619
  (j.data.sessions as SessionSummary[]).map((s) => ({ ...s, streaming: running[s.path]?.streaming === true, lastStep: running[s.path]?.lastStep })),
566
620
  );
567
621
  } catch (error) {
568
- if (alive && !(error instanceof DOMException && error.name === "AbortError"))
622
+ if (alive && !(error instanceof DOMException && error.name === "AbortError")) {
569
623
  setSessionListError(error instanceof Error ? error.message : "session list request failed");
624
+ // 会话列表没拿到(也就没拿到 registry):单独兜底拉一次工作区,侧栏不至于空着
625
+ void refreshWorkspacesFallback();
626
+ }
570
627
  } finally {
571
628
  controller = undefined;
572
629
  schedule();
@@ -586,7 +643,7 @@ export function usePiWeb() {
586
643
  controller?.abort();
587
644
  document.removeEventListener("visibilitychange", onVisibilityChange);
588
645
  };
589
- }, [sessionListRetryNonce]);
646
+ }, [sessionListRetryNonce, refreshWorkspacesFallback]);
590
647
 
591
648
  // 模型目录
592
649
  const refreshModels = useCallback(async () => {
@@ -621,14 +678,17 @@ export function usePiWeb() {
621
678
  const es = new EventSource(`/api/agent/${currentId}/events`);
622
679
  esRef.current = es;
623
680
  es.onopen = () => {
681
+ retryDelayMs = 2000;
624
682
  setState((s) => ({ ...s, connected: true }));
625
683
  };
626
684
  es.onerror = () => {
627
685
  setState((s) => ({ ...s, connected: false }));
628
- // 浏览器对非 200 响应会永久放弃 EventSource 自动重连
629
- // (网络断连才会重试),这里手动兜底:关掉后延迟重建,
630
- // 重建时会拿到全新快照,不依赖 Last-Event-ID 回放。
631
686
  if (esRef.current !== es) return;
687
+ // 网络断开时浏览器自己会重连,并带上 Last-Event-ID 让服务端从环形缓冲续放
688
+ // (只补漏掉的几条事件)。这时手动重建就等于丢掉续传、每次都拉一份全量快照。
689
+ if (es.readyState !== EventSource.CLOSED) return;
690
+ // 浏览器对非 200 响应会永久放弃自动重连:这里手动兜底,关掉后延迟重建,
691
+ // 重建时会拿到全新快照。
632
692
  es.close();
633
693
  esRef.current = null;
634
694
  const delay = retryDelayMs;
@@ -668,10 +728,10 @@ export function usePiWeb() {
668
728
  // 服务器当前 pending 集合是权威来源,刷新可恢复、已答请求不能复活。
669
729
  extensionDialogs: extensionDialogsFromSnapshot(snap, prev.extensionDialogs),
670
730
  extensionNotices: prev.extensionNotices,
671
- extensionStatuses: {},
731
+ extensionStatuses: snap.extensionStatuses ?? {},
672
732
  workingMessage: null,
673
733
  // 重连的快照不带生长账本;连接期间已收到的步保留,useGrowth 会按 seq 与拉取结果去重
674
- growth: { ...prev.growth, error: snap.growthError ?? null },
734
+ growth: { ...prev.growth, error: snap.growthError ?? null, consent: snap.growthConsent ?? null },
675
735
  }));
676
736
  } else {
677
737
  batcher.push(parsed as WebEvent);
@@ -836,6 +896,28 @@ export function usePiWeb() {
836
896
  },
837
897
  [currentId, resync, sendCommand],
838
898
  );
899
+ /** 回答「要不要在工作区建 git 仓库给生长记录用」:决定按工作区记住;失败就重连拿回服务端的待答状态 */
900
+ const answerGrowthConsent = useCallback(
901
+ async (allow: boolean) => {
902
+ const target = currentId;
903
+ if (!target) return;
904
+ setState((s) => ({ ...s, growth: { ...s.growth, consent: null } }));
905
+ try {
906
+ const r = await fetch("/api/growth", {
907
+ method: "POST",
908
+ headers: { "Content-Type": "application/json" },
909
+ body: JSON.stringify({ action: "consent", session: target, allow }),
910
+ });
911
+ const result = await r.json();
912
+ if (!r.ok || !result.success) throw new Error(result.error || `request failed (${r.status})`);
913
+ } catch (error) {
914
+ if (activeSessionRef.current !== target) return;
915
+ setState((s) => ({ ...s, error: error instanceof Error ? error.message : String(error) }));
916
+ resync();
917
+ }
918
+ },
919
+ [currentId, resync],
920
+ );
839
921
  const dismissExtensionNotice = useCallback((id: string) => setState((s) => ({ ...s, extensionNotices: s.extensionNotices.filter((n) => n.id !== id) })), []);
840
922
  const clearError = useCallback(() => setState((s) => ({ ...s, error: null })), []);
841
923
  /** 供 AppShell 直接展示行内提示(如未选工作区就发送) */
@@ -898,6 +980,9 @@ export function usePiWeb() {
898
980
  setGroupBy,
899
981
  setOrderBy,
900
982
  addWorkspaceByPicker,
983
+ dirPickerOpen,
984
+ confirmDirPicker,
985
+ cancelDirPicker,
901
986
  removeWorkspace,
902
987
  refreshModels,
903
988
  sendCommand,
@@ -909,6 +994,7 @@ export function usePiWeb() {
909
994
  clearCompaction,
910
995
  setError,
911
996
  answerExtensionDialog,
997
+ answerGrowthConsent,
912
998
  dismissExtensionNotice,
913
999
  };
914
1000
  }
package/src/i18n.tsx CHANGED
@@ -19,6 +19,7 @@ const zh = {
19
19
  error: "出错",
20
20
  copy: "复制",
21
21
  copyFailed: "复制失败",
22
+ externalImage: "外部图片,点击加载",
22
23
  chatWidthHandle: "拖动调整对话宽度",
23
24
  editMessage: "编辑这条消息",
24
25
  sendEdit: "重新发送",
@@ -145,8 +146,15 @@ const zh = {
145
146
  commandLines: "{n} 行脚本",
146
147
  thinkLabel: "思考",
147
148
  expandOutput: "展开全部输出",
149
+ expandDiff: "展开改动",
148
150
  collapseOutput: "收起",
149
151
  workspaces: "工作区",
152
+ pickFolderTitle: "选择工作区文件夹",
153
+ pickFolderUp: "上一级",
154
+ pickFolderConfirm: "选择此文件夹",
155
+ pickFolderEmpty: "此目录下没有子文件夹",
156
+ pickFolderTruncated: "只列出前 {n} 个文件夹",
157
+ pickFolderLoading: "读取中…",
150
158
  searchSessions: "搜索会话...",
151
159
  addWorkspace: "添加工作区",
152
160
  groupBy: "分组方式",
@@ -188,15 +196,10 @@ const zh = {
188
196
  noBranches: "暂无可跳转的分支节点",
189
197
  oauthLogin: "OAuth 登录",
190
198
  authMethod: "认证方式",
191
- loginTitle: "登录 PiWeb",
192
- loginDesc: "输入 PI_WEB_PASSWORD 设置的密码以继续",
193
- loginPassword: "密码",
194
- loginSubmit: "登录",
195
- loginSubmitting: "登录中…",
196
- loginFailed: "密码错误",
197
- webAuth: "密码认证",
198
- webAuthOn: "已启用(PI_WEB_PASSWORD)",
199
- webAuthOff: "未启用(仅本机可访问)",
199
+ webAuth: "访问认证",
200
+ webAuthTokenOn: "已启用(启动令牌)",
201
+ loginTokenTitle: "这个实例用启动令牌登录",
202
+ loginTokenDesc: "请打开启动 PiWeb 时终端里打印的登录链接(形如 …/?token=…)。令牌每次重启都会更换;打开一次后浏览器会记住 30 天。",
200
203
  logout: "退出登录",
201
204
  authMethodApiKey: "API 密钥",
202
205
  oauthDevicePrompt: "请在浏览器中完成授权",
@@ -375,7 +378,6 @@ const zh = {
375
378
  enterQueue: "排队:本轮结束后发送",
376
379
  enterSteer: "插话:当前工具跑完就送达",
377
380
  setSkills: "技能",
378
- // 导入会话(其它 AI 编程工具的本地对话 → pi 会话)
379
381
  importSessions: "导入会话",
380
382
  importDesc: "把本机 Codex / Claude Code / Grok / ZCode / dsh / opencode 的对话无损导入成 pi 会话:文本、思考、工具调用与结果、时间戳、用量都保留。源项目目录还在本机就导回原工作区,否则落到下面选的兜底工作区。只读源数据,不修改源工具的任何文件。",
381
383
  importRescan: "重新扫描",
@@ -397,13 +399,41 @@ const zh = {
397
399
  importRun: "导入所选 ({n})",
398
400
  importRunning: "导入中…",
399
401
  importFallbackWorkspace: "兜底工作区",
402
+ importFallbackHint: "原项目目录已不在本机时落到这里",
403
+ importWhereGoes: "导入去向",
404
+ importGoesOriginal: "回到原工作区",
405
+ importGoesFallback: "原目录已不在,用兜底工作区",
400
406
  importAlready: "已导入",
401
407
  importUntitled: "(无标题)",
402
408
  importNoProject: "(无项目目录)",
403
409
  importMessages: "{n} 条消息",
404
410
  importEmpty: "没有找到可导入的本地会话",
405
411
  importReportSummary: "成功 {imported} · 跳过 {skipped} · 失败 {failed}",
412
+ // 右侧抽屉(入口列表 + 内容)
413
+ drawerPanel: "右侧抽屉",
414
+ drawerSections: "抽屉入口",
415
+ drawerPrompts: "提示词",
406
416
  setPlugins: "插件",
417
+ setRemote: "远程连接",
418
+ remoteBuiltin: "内置插件",
419
+ remoteLanTitle: "局域网入口",
420
+ remoteLanHint: "同一个 WiFi 下的设备直接打开下面地址即可,手机排版已适配。默认开启。",
421
+ remotePublicTitle: "公网入口",
422
+ remotePublicHint: "开启后任何浏览器都能打开,无需安装客户端。地址由 Cloudflare 随机分配,每次启动都会变。",
423
+ remoteStart: "开启远程连接",
424
+ remoteStop: "关闭",
425
+ remoteWorking: "启动中…",
426
+ remoteRunning: "运行中",
427
+ remoteStopped: "未开启",
428
+ remotePending: "地址已生成,生效中…",
429
+ remoteEngineMissing: "找不到 cloudflared。请安装后重试,或用 PIWEB_CLOUDFLARED 指定它的路径。",
430
+ remoteFailed: "操作失败",
431
+ remoteBlockedAuth: "认证没开,远程连接不可用:开出去就是一个免密入口。请用 piweb 启动器启动,或设 PI_WEB_AUTH=1。",
432
+ remoteBlockedDev: "开发模式只服务本机,远程连接不可用。请先构建,再用生产模式启动。",
433
+ remoteSessionsTitle: "已登录的设备",
434
+ remoteSessionsHint: "手机、别的电脑登录后一直有效(30 天)。设备丢了或链接被别人拿到时,可以让所有设备(包括这台)都要重新用启动令牌登录。",
435
+ remoteSignOutAll: "让所有设备重新登录",
436
+ remoteSignOutAllConfirm: "再点一次确认",
407
437
  appearance: "外观",
408
438
  appearanceMode: "亮暗模式",
409
439
  themeLight: "浅色",
@@ -420,6 +450,12 @@ const zh = {
420
450
  fontUiSizeDesc: "侧栏、按钮、标签等界面文字",
421
451
  fontChatSize: "对话字体",
422
452
  fontChatSizeDesc: "消息正文、用户气泡与输入框",
453
+ fontUiFamily: "界面字体族",
454
+ fontUiFamilyDesc: "侧栏、按钮、标签等界面文字使用的字体",
455
+ fontMonoFamily: "代码字体族",
456
+ fontMonoFamilyDesc: "代码块、diff、行内代码使用的等宽字体",
457
+ fontCustom: "自定义…",
458
+ fontCustomPlaceholder: "输入字体栈,如 Fira Code, monospace",
423
459
  language: "界面语言",
424
460
  openConfigFile: "打开配置文件",
425
461
  providerAuthOk: "已配置",
@@ -433,7 +469,7 @@ const zh = {
433
469
  apiKey: "API Key",
434
470
  apiKeyStored: "已保存;留空保持不变",
435
471
  providerIdImmutable: "Provider ID 创建后不可修改;如需更名,请删除后重建。",
436
- modelConfigReopen: "模型配置已保存。新建会话或重启 PiWeb 后,当前运行时才能使用新配置。",
472
+ modelConfigReopen: "模型配置已保存并立即生效;正在回答的会话在本轮结束后生效。",
437
473
  customModelIdentityNotice: "自定义模型 ID 只是发往该 API 地址的路由名;PiWeb 不会自动下载、接入或识别本机 Ollama 模型。",
438
474
  customApiKeyHint: "可留空改用 auth.json;指向本机/私网的无鉴权服务(Ollama 等)保存时自动填占位值。",
439
475
  modelsTitle: "模型",
@@ -557,8 +593,10 @@ const zh = {
557
593
  growthScopeRound: "本轮",
558
594
  growthScopeSession: "本会话累计",
559
595
  growthOnlyChanges: "只看改动",
560
- growthFollow: "跟随",
561
- growthFollowing: "跟随中",
596
+ // 轮数选择器:计数可点开轮次列表(不再有「跟随」开关)
597
+ growthPickRound: "选择轮次",
598
+ growthRoundJump: "输入轮号...",
599
+ growthRoundJumpHint: "输入轮号后按回车跳到该轮",
562
600
  growthPrev: "上一轮",
563
601
  growthNext: "下一轮",
564
602
  growthRoundPosition: "第 {current}/{total} 轮",
@@ -570,6 +608,12 @@ const zh = {
570
608
  growthNoSession: "打开或新建一个会话后,这里会显示项目的生长过程。",
571
609
  growthUnavailable: "机器上找不到 git,项目生长功能不可用",
572
610
  growthUnavailableFallback: "生长记录不可用(找不到可用的 git),已显示当前目录结构",
611
+ growthConsentTitle: "要为这个工作区开启生长记录吗?",
612
+ growthConsentNotGit: "生长记录会把 pi 每一轮的改动存成 git 提交,方便回看和对比。这个目录还不是 git 仓库,开启后会在这里执行 git init(新建 .git 目录)。",
613
+ growthConsentNested: "生长记录会把 pi 每一轮的改动存成 git 提交,方便回看和对比。这个目录是另一个仓库({parent})的子目录,开启后会在这里再建一个嵌套的 git 仓库,外层仓库会把它当成一个子仓库看待。",
614
+ growthConsentNote: "记录挂在专用引用 refs/piweb/rounds/… 上,不动分支和暂存区。选择会按工作区记住。",
615
+ growthConsentAllow: "开启并建仓库",
616
+ growthConsentDecline: "不记录",
573
617
  growthRoundLabel: "第 {n} 轮",
574
618
  growthKindUser: "你的修改",
575
619
  growthRoundFiles: "{n} 个文件",
@@ -625,6 +669,7 @@ const en: Dict = {
625
669
  error: "Error",
626
670
  copy: "Copy",
627
671
  copyFailed: "Copy failed",
672
+ externalImage: "External image — click to load",
628
673
  chatWidthHandle: "Drag to resize the conversation",
629
674
  editMessage: "Edit this message",
630
675
  sendEdit: "Send again",
@@ -750,8 +795,15 @@ const en: Dict = {
750
795
  commandLines: "{n}-line script",
751
796
  thinkLabel: "Thought",
752
797
  expandOutput: "Show full output",
798
+ expandDiff: "Show changes",
753
799
  collapseOutput: "Collapse",
754
800
  workspaces: "Workspaces",
801
+ pickFolderTitle: "Choose a workspace folder",
802
+ pickFolderUp: "Parent folder",
803
+ pickFolderConfirm: "Use this folder",
804
+ pickFolderEmpty: "No subfolders here",
805
+ pickFolderTruncated: "Showing the first {n} folders",
806
+ pickFolderLoading: "Loading…",
755
807
  searchSessions: "Search sessions...",
756
808
  addWorkspace: "Add workspace",
757
809
  groupBy: "Group by",
@@ -793,15 +845,10 @@ const en: Dict = {
793
845
  noBranches: "No branch points available",
794
846
  oauthLogin: "OAuth login",
795
847
  authMethod: "Authentication",
796
- loginTitle: "Sign in to PiWeb",
797
- loginDesc: "Enter the password configured in PI_WEB_PASSWORD to continue",
798
- loginPassword: "Password",
799
- loginSubmit: "Sign in",
800
- loginSubmitting: "Signing in…",
801
- loginFailed: "Incorrect password",
802
- webAuth: "Password auth",
803
- webAuthOn: "Enabled (PI_WEB_PASSWORD)",
804
- webAuthOff: "Disabled (local access only)",
848
+ webAuth: "Access authentication",
849
+ webAuthTokenOn: "Enabled (launch token)",
850
+ loginTokenTitle: "This instance signs in with a launch token",
851
+ loginTokenDesc: "Open the login link printed in the terminal when PiWeb started (…/?token=…). The token changes on every restart; once opened, the browser remembers it for 30 days.",
805
852
  logout: "Sign out",
806
853
  authMethodApiKey: "API key",
807
854
  oauthDevicePrompt: "Finish authorization in your browser",
@@ -974,11 +1021,10 @@ const en: Dict = {
974
1021
  enterQueue: "Queue: sent after this turn ends",
975
1022
  enterSteer: "Steer: delivered after the current tool call",
976
1023
  setSkills: "Skills",
977
- // Session import (other AI coding tools' local conversations → pi sessions)
978
1024
  importSessions: "Import sessions",
979
1025
  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.",
980
1026
  importRescan: "Rescan",
981
- importScanning: "Scanning…",
1027
+ importScanning: "Scanning...",
982
1028
  importSourceClaude: "Claude Code",
983
1029
  importSourceCodex: "Codex",
984
1030
  importSourceGrok: "Grok",
@@ -994,15 +1040,42 @@ const en: Dict = {
994
1040
  importSelectNone: "Clear group",
995
1041
  importClearSelection: "Clear",
996
1042
  importRun: "Import selected ({n})",
997
- importRunning: "Importing…",
1043
+ importRunning: "Importing...",
998
1044
  importFallbackWorkspace: "Fallback workspace",
1045
+ importFallbackHint: "Used when the original project folder is gone",
1046
+ importWhereGoes: "Destination",
1047
+ importGoesOriginal: "Back to its original workspace",
1048
+ importGoesFallback: "Original folder is gone — uses the fallback workspace",
999
1049
  importAlready: "Imported",
1000
1050
  importUntitled: "(untitled)",
1001
1051
  importNoProject: "(no project directory)",
1002
1052
  importMessages: "{n} messages",
1003
1053
  importEmpty: "No importable local sessions found",
1004
1054
  importReportSummary: "Imported {imported} · skipped {skipped} · failed {failed}",
1055
+ drawerPanel: "Drawer",
1056
+ drawerSections: "Drawer sections",
1057
+ drawerPrompts: "Prompts",
1005
1058
  setPlugins: "Plugins",
1059
+ setRemote: "Remote access",
1060
+ remoteBuiltin: "Built-in plugin",
1061
+ remoteLanTitle: "Local network",
1062
+ remoteLanHint: "Devices on the same Wi-Fi can open the address below; the mobile layout is already applied. On by default.",
1063
+ remotePublicTitle: "Public access",
1064
+ remotePublicHint: "Once on, any browser can open it with nothing to install. Cloudflare assigns a random address that changes on every start.",
1065
+ remoteStart: "Turn on",
1066
+ remoteStop: "Turn off",
1067
+ remoteWorking: "Starting…",
1068
+ remoteRunning: "Running",
1069
+ remoteStopped: "Off",
1070
+ remotePending: "Address created, taking effect…",
1071
+ remoteEngineMissing: "cloudflared not found. Install it and retry, or point PIWEB_CLOUDFLARED at it.",
1072
+ remoteFailed: "Action failed",
1073
+ remoteBlockedAuth: "Remote access is unavailable because authentication is off — it would be an open, password-free entry. Start with the piweb launcher, or set PI_WEB_AUTH=1.",
1074
+ remoteBlockedDev: "Development mode serves this machine only, so remote access is unavailable. Build first, then start in production mode.",
1075
+ remoteSessionsTitle: "Signed-in devices",
1076
+ remoteSessionsHint: "Phones and other computers stay signed in for 30 days. If a device is lost or a link leaked, make every device (including this one) sign in again with the launch token.",
1077
+ remoteSignOutAll: "Sign out all devices",
1078
+ remoteSignOutAllConfirm: "Click again to confirm",
1006
1079
  appearance: "Appearance",
1007
1080
  appearanceMode: "Light / Dark",
1008
1081
  themeLight: "Light",
@@ -1019,6 +1092,12 @@ const en: Dict = {
1019
1092
  fontUiSizeDesc: "Sidebar, buttons, labels and other UI text",
1020
1093
  fontChatSize: "Chat font size",
1021
1094
  fontChatSizeDesc: "Message body, user bubbles and the composer",
1095
+ fontUiFamily: "UI font family",
1096
+ fontUiFamilyDesc: "Font used by the sidebar, buttons, labels and other UI text",
1097
+ fontMonoFamily: "Code font family",
1098
+ fontMonoFamilyDesc: "Monospace font used by code blocks, diffs and inline code",
1099
+ fontCustom: "Custom…",
1100
+ fontCustomPlaceholder: "Font stack, e.g. Fira Code, monospace",
1022
1101
  language: "Language",
1023
1102
  openConfigFile: "Open config file",
1024
1103
  providerAuthOk: "Configured",
@@ -1032,7 +1111,7 @@ const en: Dict = {
1032
1111
  apiKey: "API Key",
1033
1112
  apiKeyStored: "Saved; leave blank to keep it",
1034
1113
  providerIdImmutable: "A Provider ID cannot be changed after creation. Delete and recreate it to rename it.",
1035
- modelConfigReopen: "Model configuration saved. Start a new session or restart PiWeb before using it in the current runtime.",
1114
+ modelConfigReopen: "Model configuration saved and applied. Sessions that are answering pick it up after the current turn.",
1036
1115
  customModelIdentityNotice: "A custom model ID is only a route name sent to that API endpoint. PiWeb does not automatically download, attach, or identify local Ollama models.",
1037
1116
  customApiKeyHint: "Leave blank to use auth.json. Keyless local/private services (Ollama etc.) get a placeholder key automatically on save.",
1038
1117
  modelsTitle: "Models",
@@ -1155,8 +1234,10 @@ const en: Dict = {
1155
1234
  growthScopeRound: "This round",
1156
1235
  growthScopeSession: "Session total",
1157
1236
  growthOnlyChanges: "Changes only",
1158
- growthFollow: "Follow",
1159
- growthFollowing: "Following",
1237
+ // Round picker: the counter opens a round list (no "Follow" switch any more)
1238
+ growthPickRound: "Select round",
1239
+ growthRoundJump: "Enter round number...",
1240
+ growthRoundJumpHint: "Type a round number and press Enter to jump",
1160
1241
  growthPrev: "Previous round",
1161
1242
  growthNext: "Next round",
1162
1243
  growthRoundPosition: "Round {current}/{total}",
@@ -1168,6 +1249,12 @@ const en: Dict = {
1168
1249
  growthNoSession: "Open or start a session to watch the project grow here.",
1169
1250
  growthUnavailable: "git was not found on this machine; project growth is unavailable",
1170
1251
  growthUnavailableFallback: "Growth history is unavailable (no usable git); showing the current directory structure",
1252
+ growthConsentTitle: "Turn on growth history for this workspace?",
1253
+ growthConsentNotGit: "Growth history saves each pi turn's changes as git commits so you can review and compare them. This folder is not a git repository yet; turning it on runs git init here (creates a .git folder).",
1254
+ growthConsentNested: "Growth history saves each pi turn's changes as git commits so you can review and compare them. This folder is inside another repository ({parent}); turning it on creates a nested git repository here, which the outer repository will see as a sub-repository.",
1255
+ growthConsentNote: "Commits live on a private ref (refs/piweb/rounds/…) and never touch your branches or index. The choice is remembered per workspace.",
1256
+ growthConsentAllow: "Turn on and create repository",
1257
+ growthConsentDecline: "Don't record",
1171
1258
  growthRoundLabel: "Round {n}",
1172
1259
  growthKindUser: "Your changes",
1173
1260
  growthRoundFiles: "{n} files",
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 服务启动钩子的 Node 部分(只由 instrumentation.ts 在 nodejs 运行时引入)。
3
+ *
4
+ * 这里用到 node:child_process 等模块,edge 运行时编不过;放在单独文件里,
5
+ * edge 那份编译就整段跳过了(见 instrumentation.ts)。
6
+ */
7
+ export async function registerNode() {
8
+ // 构建期也会实例化服务,别在构建机上开监听
9
+ if (process.env.NEXT_PHASE === "phase-production-build") return;
10
+
11
+ // 再次运行启动器时要能找回本实例的登录链接(见 publishLaunchToken)
12
+ const { authEnabled } = await import("./lib/web-auth");
13
+ if (authEnabled()) (await import("./lib/web-launch-token")).publishLaunchToken();
14
+
15
+ const { remoteBlocked, remoteLanEnabled, startLan } = await import("./lib/remote/supervisor");
16
+ if (!remoteLanEnabled()) return;
17
+
18
+ // 局域网入口会把 PiWeb 暴露给同网段的所有设备:没认证、或是开发模式,都绝不能开。
19
+ const blocked = remoteBlocked();
20
+ if (blocked === "auth") {
21
+ console.warn("[远程连接] 已跳过局域网入口:认证没开。请设 PI_WEB_AUTH=1。");
22
+ return;
23
+ }
24
+ if (blocked === "dev") {
25
+ console.warn("[远程连接] 已跳过局域网入口:开发模式只服务本机(认证保护不了 Next 开发工具端点)。");
26
+ return;
27
+ }
28
+
29
+ const state = await startLan();
30
+ if (state.running) {
31
+ console.log(`[远程连接] 局域网入口已就绪:${state.urls.join(" ")}`);
32
+ } else {
33
+ console.warn(`[远程连接] 局域网入口启动失败:${state.error}`);
34
+ }
35
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 服务启动钩子:「远程连接」内置插件的局域网入口在这里拉起。
3
+ *
4
+ * 为什么必须在这里:局域网入口要「默认就在跑」(手机连同一个 WiFi 就能用),
5
+ * 而 Next 的请求处理里没有「服务刚起来」这个时机 —— instrumentation 的 register()
6
+ * 是官方给的入口,而且它返回前服务不会开始处理请求。
7
+ *
8
+ * Next 会把本文件按 nodejs 和 edge 各编译一份。Node 专用的代码只能写在
9
+ * `=== "nodejs"` 的分支里再 import:edge 那份里条件是常量 false,webpack 整段跳过。
10
+ * 写成「不是 nodejs 就 return,后面再 import」不行 —— webpack 照样解析后面的 import,
11
+ * edge 编译碰到 node:child_process 就报错,dev 下所有页面跟着 500。
12
+ */
13
+ export async function register() {
14
+ if (process.env.NEXT_RUNTIME === "nodejs") {
15
+ const { registerNode } = await import("./instrumentation-node");
16
+ await registerNode();
17
+ }
18
+ }