pi-webapp 0.1.5 → 0.1.7

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 (126) hide show
  1. package/README.md +16 -3
  2. package/README.zh-CN.md +14 -3
  3. package/THIRD_PARTY_LICENSE.txt +24 -0
  4. package/bin/pi-webapp.mjs +93 -5
  5. package/dist/extension.js +9 -6
  6. package/package.json +28 -5
  7. package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
  8. package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
  9. package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
  10. package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
  11. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
  12. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
  13. package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
  14. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
  15. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
  16. package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
  17. package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
  18. package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
  19. package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
  20. package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
  21. package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
  22. package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
  23. package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
  24. package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
  25. package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
  26. package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
  27. package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
  28. package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
  29. package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
  30. package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
  31. package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
  32. package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
  33. package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
  34. package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
  35. package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
  36. package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
  37. package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
  38. package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
  39. package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
  40. package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
  41. package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
  42. package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
  43. package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
  44. package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
  45. package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
  46. package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
  47. package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
  48. package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
  49. package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
  50. package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
  51. package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
  52. package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
  53. package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
  54. package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
  55. package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
  56. package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
  57. package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
  58. package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
  59. package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
  60. package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
  61. package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
  62. package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
  63. package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
  64. package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
  65. package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
  66. package/web/dist/assets/app.js +504 -55
  67. package/web/dist/assets/asciidoc.js +1 -0
  68. package/web/dist/assets/bat.js +1 -0
  69. package/web/dist/assets/bibtex.js +1 -0
  70. package/web/dist/assets/c.js +1 -0
  71. package/web/dist/assets/clojure.js +1 -0
  72. package/web/dist/assets/cmake.js +1 -0
  73. package/web/dist/assets/cpp.js +1 -0
  74. package/web/dist/assets/csharp.js +1 -0
  75. package/web/dist/assets/css.js +1 -0
  76. package/web/dist/assets/csv.js +1 -0
  77. package/web/dist/assets/dart.js +1 -0
  78. package/web/dist/assets/diff.js +1 -0
  79. package/web/dist/assets/dotenv.js +1 -0
  80. package/web/dist/assets/elixir.js +1 -0
  81. package/web/dist/assets/erlang.js +1 -0
  82. package/web/dist/assets/fish.js +1 -0
  83. package/web/dist/assets/fsharp.js +1 -0
  84. package/web/dist/assets/go.js +1 -0
  85. package/web/dist/assets/graphql.js +1 -0
  86. package/web/dist/assets/groovy.js +1 -0
  87. package/web/dist/assets/haskell.js +1 -0
  88. package/web/dist/assets/hcl.js +1 -0
  89. package/web/dist/assets/html-derivative.js +1 -0
  90. package/web/dist/assets/html.js +1 -0
  91. package/web/dist/assets/http.js +1 -0
  92. package/web/dist/assets/ini.js +1 -0
  93. package/web/dist/assets/java.js +1 -0
  94. package/web/dist/assets/javascript.js +1 -0
  95. package/web/dist/assets/julia.js +1 -0
  96. package/web/dist/assets/kotlin.js +1 -0
  97. package/web/dist/assets/latex.js +1 -0
  98. package/web/dist/assets/less.js +1 -0
  99. package/web/dist/assets/log.js +1 -0
  100. package/web/dist/assets/lua.js +1 -0
  101. package/web/dist/assets/make.js +1 -0
  102. package/web/dist/assets/markdown.js +1 -0
  103. package/web/dist/assets/mdx.js +1 -0
  104. package/web/dist/assets/nix.js +1 -0
  105. package/web/dist/assets/perl.js +1 -0
  106. package/web/dist/assets/php.js +1 -0
  107. package/web/dist/assets/powershell.js +1 -0
  108. package/web/dist/assets/proto.js +1 -0
  109. package/web/dist/assets/python.js +1 -0
  110. package/web/dist/assets/r.js +1 -0
  111. package/web/dist/assets/rst.js +1 -0
  112. package/web/dist/assets/ruby.js +1 -0
  113. package/web/dist/assets/rust.js +1 -0
  114. package/web/dist/assets/scala.js +1 -0
  115. package/web/dist/assets/scss.js +1 -0
  116. package/web/dist/assets/sql.js +1 -0
  117. package/web/dist/assets/style.css +1 -1
  118. package/web/dist/assets/svelte.js +1 -0
  119. package/web/dist/assets/swift.js +1 -0
  120. package/web/dist/assets/system-verilog.js +1 -0
  121. package/web/dist/assets/toml.js +1 -0
  122. package/web/dist/assets/vb.js +1 -0
  123. package/web/dist/assets/verilog.js +1 -0
  124. package/web/dist/assets/vue.js +1 -0
  125. package/web/dist/assets/xml.js +1 -0
  126. package/web/dist/assets/yaml.js +1 -0
package/README.md CHANGED
@@ -16,6 +16,8 @@ npx pi-webapp
16
16
 
17
17
  This starts Pi in the background and opens the default browser on a local graphical desktop, leaving the terminal free. If Pi is missing, the launcher installs the official `@earendil-works/pi-coding-agent` package with npm. An existing Pi installation must be version 0.87.1 or later. The launcher does not replace an older installation automatically. Running the launcher again reopens the existing server. Remote or headless sessions print the address instead; set `PI_WEBAPP_AUTO_OPEN=0` to disable automatic opening.
18
18
 
19
+ In Settings → General → App updates, the web page checks npm for a newer `pi-webapp` release. When started by the background launcher from an npm package, **Update and restart** installs the verified release under Pi's `pi-web/releases/` data directory, restarts the server, and reloads the same browser address. Wait for any active reply to finish first. If the new version cannot start, the launcher restores the previous service. This works for both `npx` and global-command launches; later launches select the managed release automatically. It does not change the npm global installation. Source checkouts and `/web` pages opened from an existing Pi terminal show version information but cannot restart from the web page. If the service does not return, run `pi-webapp status` and check `~/.pi/agent/pi-web/launcher.log`.
20
+
19
21
  Use `npx pi-webapp status` to show the address and `npx pi-webapp stop` to stop the background service. After a global install, use `pi-webapp status` and `pi-webapp stop`.
20
22
 
21
23
  You can also install the launcher globally:
@@ -37,12 +39,12 @@ In Pi, enter `/web` to open the interface in your default browser. Pi also print
37
39
  ## Features
38
40
 
39
41
  - View the current conversation with Markdown, streaming output, expandable reasoning and tool calls, token usage, and a turn-by-turn trace.
40
- - Send messages, stop a run, start a new Pi session, and change the current model or thinking level.
42
+ - Send messages, pause and continue a run, start a new Pi session, and change the current model or thinking level. The bottom-right control pauses an active run; an empty composer offers Continue after pausing, including after refreshing or reopening the session. Continue uses an internal control action without adding a user message; the model makes a new request based on existing progress, and plan mode still requires fresh approval.
41
43
  - Browse workspaces and saved sessions. Add a workspace with the native directory picker when available, or use the built-in directory browser over SSH.
42
44
  - Attach up to 20 files of 20 MB each by selecting, dropping, or pasting them. Images show previews and are also sent as Pi image content.
43
45
  - Manage appearance, Pi packages, extensions, skills, and models from the settings panel. The Models page can add models, edit display names, context windows, output limits, and input types, and restore built-in defaults; changes are saved to Pi's `models.json`. Browser sign-in opens the provider's authorization page automatically; manual code entry remains available when a callback cannot reach Pi. Stored provider credentials can also be removed there. Credentials stay in Pi's local `auth.json` and are never returned to the browser.
44
46
 
45
- If you enter `/web` manually in a Pi terminal, that terminal session stays synchronized with the browser. With the launcher, Pi runs in the background. Sessions opened in other workspaces use the Pi SDK and load that workspace's skills and project context. They do not load extensions again in the same process.
47
+ If you enter `/web` manually in a Pi terminal, that terminal session stays synchronized with the browser. With the launcher, Pi runs in the background. Sessions opened in other workspaces use the Pi SDK and load that workspace's skills and project context. They load enabled Web plugin Pi entries explicitly while keeping other Pi extensions isolated from the parent bridge.
46
48
 
47
49
  On startup, the app discovers workspaces from Pi's saved sessions, so conversations from other directories appear in the sidebar. Workspace records and attachments stay under Pi's existing `pi-web/` data directory. Removing a workspace from the sidebar does not delete its files or sessions; you can add it again manually.
48
50
 
@@ -60,13 +62,24 @@ npm run stop
60
62
 
61
63
  `npm run start` builds the project, starts Pi in the background, and opens the web interface. After editing the extension, run `npm run stop` followed by `npm run start`. If you develop with `pi -e .`, enter `/reload` and `/web` in Pi after rebuilding. Refreshing an old browser page alone does not replace the running bridge.
62
64
 
65
+ For local Web plugin development with the sibling `pi-extensions` workspace, follow its `README.md`. Set `PI_WEBAPP_PLUGIN_DEV_ROOTS=../pi-extensions/packages/plan-mode` when starting this app; browser asset changes refresh automatically and Pi entry changes trigger a safe reload when the agent is idle.
66
+
63
67
  The source lives in `extension/`, `shared/`, and `web/src/`. The npm package contains only the bundled, minified JavaScript extension, built web assets, cover image, and the English and Chinese READMEs. No TypeScript source or source maps are published. Minification makes the shipped code harder to read; it does not encrypt JavaScript.
64
68
 
65
69
  ## Publish
66
70
 
71
+ CI resolves the published `@chengzhiyi/pi-web-protocol` version before `npm ci`.
72
+ Until the first protocol publication, verification builds a pinned `pi-extensions`
73
+ commit in an ignored `.ci/` checkout and uses that local package. Checks, tests,
74
+ and builds still run; the Actions summary explains why host publication is waiting.
75
+ Release builds always require the published npm dependency. Once it exists, the
76
+ next workflow uses its exact version and the release commit records the lockfile.
77
+ Use Actions' manual run after initializing npm; protocol releases do not trigger
78
+ this repository automatically.
79
+
67
80
  `npm publish` runs the checks, tests, and production build. The `pi-package` keyword makes the published npm package eligible for the [Pi package catalog](https://pi.dev/packages); catalog updates may lag behind npm.
68
81
 
69
- GitHub Actions runs `check`, `test`, and `build` on pull requests to `main`. A push to `main` repeats those checks and then publishes the next patch version to npm. The release version is selected from the greater of the source version and the npm `latest` version; it is set in the package during CI and is not committed back to Git. To start a new minor or major series, raise the version in `package.json` and `package-lock.json` in the pull request. Each release also updates the package's `pi.image` URL to its own version.
82
+ GitHub Actions runs `check`, `test`, and `build` on pull requests to `main`. A push to `main` repeats those checks, chooses the next patch version from the source version and npm `latest`, then commits the updated `package.json` and `package-lock.json` to `main` before publishing. The release commit uses the workflow's `GITHUB_TOKEN` and does not start another workflow run. To start a new minor or major series, raise the version in both package files in the pull request. Each release also updates the package's `pi.image` URL to its own version. The repository must allow the workflow to push to `main` for publishing to proceed.
70
83
 
71
84
  Before the first automated release, configure npm Trusted Publishing for the `pi-webapp` package: GitHub owner `chengzhiyi`, repository `pi-webapp`, workflow filename `ci.yml`, no environment, and allow direct `npm publish`. This workflow uses GitHub's OIDC identity and does not need an npm token. Keep the package's repository URL in `package.json` aligned with the GitHub repository.
72
85
 
package/README.zh-CN.md CHANGED
@@ -16,6 +16,8 @@ npx pi-webapp
16
16
 
17
17
  该命令会在后台启动 Pi,并在本地图形桌面用默认浏览器打开网页,终端可继续使用。若未找到 Pi,会自动通过 npm 安装官方 `@earendil-works/pi-coding-agent`。已有 Pi 需要 0.87.1 或更新版本;启动器不会自动覆盖旧版本。再次运行启动命令会重新打开已有服务。远程或无图形界面时只显示地址;设置 `PI_WEBAPP_AUTO_OPEN=0` 可关闭自动打开。
18
18
 
19
+ 网页的「设置 → 通用设置 → 应用更新」会检查 npm 上的 `pi-webapp` 新版本。通过 npm 包的后台启动器运行时,点击「升级并重启」会先将新版安装并校验到 Pi 数据目录的 `pi-web/releases/`,然后重启服务,网页会在原地址重新加载。请先等待当前回复结束。若新版无法启动,启动器会恢复旧版服务。`npx` 和全局命令启动均可使用;后续启动会自动选用已安装的新版。此操作不会改动 npm 全局安装。源码目录运行和已有 Pi 终端中的 `/web` 页面可以查看版本,但不能从网页重启。若服务未恢复,可运行 `pi-webapp status` 并查看 `~/.pi/agent/pi-web/launcher.log`。
20
+
19
21
  可用 `npx pi-webapp status` 查看地址,用 `npx pi-webapp stop` 停止后台服务。全局安装后可直接运行 `pi-webapp status` 和 `pi-webapp stop`。
20
22
 
21
23
  也可以将启动器安装为全局命令:
@@ -37,12 +39,12 @@ pi
37
39
  ## 功能
38
40
 
39
41
  - 查看当前对话,包括 Markdown 内容、流式输出、可展开的推理过程与工具调用、Token 用量,以及逐轮执行轨迹。
40
- - 发送消息、停止运行、启动新的 Pi 会话,以及切换当前模型或思考级别。
42
+ - 发送消息、暂停与继续运行、启动新的 Pi 会话,以及切换当前模型或思考级别。运行时右下角显示暂停;暂停后清空输入框即可继续,刷新或重新打开会话后仍可恢复。继续通过内部控制动作恢复原任务,不新增用户消息;模型会基于已有进度发起新请求,计划模式仍需重新审批。
41
43
  - 浏览工作区和已保存的会话。环境支持时可通过系统目录选择器添加工作区;通过 SSH 使用时,也可使用内置目录浏览器。
42
44
  - 通过选择、拖放或粘贴附加文件,最多 20 个,每个不超过 20 MB。图片会显示预览,并作为 Pi 图片内容发送。
43
45
  - 在设置面板中管理外观、Pi 软件包、扩展、技能和模型。模型页可添加模型、编辑显示名称、上下文窗口、最大输出 token 数和输入类型,或恢复内置模型的默认参数;配置写入 Pi 的 `models.json`。网页登录会自动打开提供方授权页;当回调无法到达 Pi 时,仍可手动粘贴授权码。已保存的提供方认证可在这里移除。凭据保留在 Pi 本地的 `auth.json`,不会返回给浏览器。
44
46
 
45
- 如果通过 Pi 终端手动输入 `/web`,该终端会话会与浏览器保持同步。使用启动器时,Pi 在后台运行。在其他工作区打开的会话使用 Pi SDK,并加载该工作区的技能和项目上下文;同一进程中不会再次加载扩展。
47
+ 如果通过 Pi 终端手动输入 `/web`,该终端会话会与浏览器保持同步。使用启动器时,Pi 在后台运行。在其他工作区打开的会话使用 Pi SDK,并加载该工作区的技能和项目上下文;已启用的 Web 插件 Pi 入口会显式加载,其他普通 Pi 扩展仍与主会话隔离。
46
48
 
47
49
  启动时会从 Pi 已保存的会话中发现工作区,因此先前在其他目录使用 Pi 的会话也会出现在侧边栏。工作区记录和附件继续存放在 Pi 现有的 `pi-web/` 数据目录下。从侧边栏移除工作区不会删除其文件或会话;移除后如需再次显示,可以手动添加。
48
50
 
@@ -60,13 +62,22 @@ npm run stop
60
62
 
61
63
  `npm run start` 会构建项目、在后台启动 Pi 并打开 Web 界面。修改扩展后,运行 `npm run stop`,再运行 `npm run start`;如果使用 `pi -e .` 开发,则在 Pi 中依次输入 `/reload` 和 `/web`。仅刷新旧的浏览器页面不会替换正在运行的连接桥接代码。
62
64
 
65
+ 与同级 `pi-extensions` 联调时,先在该目录运行 `pnpm install && pnpm build`,再运行 `pnpm dev`;在本项目设置 `PI_WEBAPP_PLUGIN_DEV_ROOTS=../pi-extensions/packages/plan-mode` 后执行 `npm run start`。浏览器资源变化后页面会刷新,Pi 入口变化后会等待代理空闲并重载扩展。完整步骤见 `pi-extensions/README.md`。
66
+
63
67
  源代码位于 `extension/`、`shared/` 和 `web/src/`。npm 软件包仅包含打包并压缩后的 JavaScript 扩展、构建后的 Web 资源、封面图片,以及英文和中文 README;不会发布 TypeScript 源码或 source map。代码压缩会增加阅读发布版本代码的难度,但不会对 JavaScript 加密。
64
68
 
65
69
  ## 发布
66
70
 
71
+ CI 会先从 npm 解析 `@chengzhiyi/pi-web-protocol` 的已发布版本,再运行 `npm ci`。
72
+ 首次发布协议前,校验任务会将固定提交的 `pi-extensions` 检出到被忽略的 `.ci/`
73
+ 目录,构建协议并使用本地包,完整执行检查、测试与构建;Actions 摘要会说明
74
+ 宿主发布仍在等待协议初始化。正式发布始终要求准确的 npm 依赖,并将清单和
75
+ 锁文件变化同步回仓库。协议发布后可以在 Actions 手动运行工作流;协议发布
76
+ 不会主动触发这个仓库。
77
+
67
78
  `npm publish` 会运行检查、测试和生产构建。`pi-package` 关键词使发布到 npm 的软件包有资格进入 [Pi 软件包目录](https://pi.dev/packages);目录更新可能晚于 npm。
68
79
 
69
- GitHub Actions 会在针对 `main` 的拉取请求中运行 `check`、`test` 和 `build`。代码推送到 `main` 后会再次运行这些检查,全部通过才自动发布下一个补丁版本。发布版本根据源码版本和 npm `latest` 版本确定,仅在 CI 中写入包文件,不会提交回 Git。如需开始新的次版本或主版本,请在拉取请求中同步提高 `package.json` 和 `package-lock.json` 的版本。每次发布也会将包内 `pi.image` 的地址更新为该版本。
80
+ GitHub Actions 会在针对 `main` 的拉取请求中运行 `check`、`test` 和 `build`。代码推送到 `main` 后会再次运行这些检查,根据源码版本和 npm `latest` 版本确定下一个补丁版本,先将更新后的 `package.json` 和 `package-lock.json` 提交回 `main`,再发布到 npm。版本同步提交使用工作流的 `GITHUB_TOKEN`,不会再次触发工作流。如需开始新的次版本或主版本,请在拉取请求中同步提高两个包文件的版本。每次发布也会将包内 `pi.image` 的地址更新为该版本。仓库必须允许工作流推送到 `main`,发布才能继续。
70
81
 
71
82
  首次自动发布前,请在 npm 的 `pi-webapp` 包设置中配置 Trusted Publishing:GitHub 所有者为 `chengzhiyi`,仓库为 `pi-webapp`,工作流文件名为 `ci.yml`,不设置环境,并允许直接执行 `npm publish`。工作流通过 GitHub OIDC 认证,无需 npm token。`package.json` 中的仓库地址应与 GitHub 仓库保持一致。
72
83
 
@@ -0,0 +1,24 @@
1
+ DSH presentation components are derived from DeepSeek Harness, commit
2
+ 477b4f420553e8a52c2fbccc464d7561b239c443.
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2026 DeepSeek
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
package/bin/pi-webapp.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn as spawnNode } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
- import { closeSync, existsSync, openSync } from "node:fs";
4
+ import { closeSync, existsSync, openSync, watch } from "node:fs";
5
5
  import { mkdir, open, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
6
6
  import { homedir } from "node:os";
7
- import { dirname, join, resolve } from "node:path";
7
+ import { delimiter, dirname, join, resolve } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import spawn from "cross-spawn";
10
10
  import { openWebPage } from "./open-web-page.mjs";
@@ -15,6 +15,8 @@ const extension = fileURLToPath(new URL("../dist/extension.js", import.meta.url)
15
15
  const agentDir = resolve((process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent")).replace(/^~(?=$|[\\/])/, homedir()));
16
16
  const statePath = join(agentDir, "pi-web", "launcher.json");
17
17
  const stopPath = join(agentDir, "pi-web", "launcher.stop");
18
+ const restartPath = join(agentDir, "pi-web", "launcher.restart");
19
+ const updatePath = join(agentDir, "pi-web", "update.json");
18
20
  const logPath = join(agentDir, "pi-web", "launcher.log");
19
21
 
20
22
  function versionParts(value) {
@@ -111,8 +113,9 @@ async function serve(pi, args) {
111
113
  return;
112
114
  } finally { await lock?.close().catch(() => {}); }
113
115
 
114
- const child = spawn(pi, ["--mode", "rpc", "-e", extension, ...args], {
115
- cwd: process.cwd(), env: { ...process.env, PI_WEBAPP_AUTO_OPEN: "0" }, stdio: ["pipe", "pipe", "pipe"],
116
+ const pluginRoots = (process.env.PI_WEBAPP_PLUGIN_DEV_ROOTS ?? "").split(delimiter).filter(Boolean).map((root) => resolve(root));
117
+ const child = spawn(pi, ["--mode", "rpc", "-e", extension, ...pluginRoots.flatMap((root) => ["-e", root]), ...args], {
118
+ cwd: process.cwd(), env: { ...process.env, PI_WEBAPP_AUTO_OPEN: "0", PI_WEBAPP_LAUNCHER_NONCE: nonce }, stdio: ["pipe", "pipe", "pipe"],
116
119
  });
117
120
  child.stderr?.pipe(process.stderr, { end: false });
118
121
  let ready = false;
@@ -121,8 +124,14 @@ async function serve(pi, args) {
121
124
  let closed = false;
122
125
  let stateWrite = Promise.resolve();
123
126
  let stopping = false;
127
+ let restartRequest = null;
128
+ let runningUrl = null;
124
129
  let buffer = "";
125
130
  let killTimer;
131
+ const devWatchers = [];
132
+ let devReloadTimer;
133
+ let devReloadCount = 0;
134
+ let devReloadCommand;
126
135
  const shutdown = () => {
127
136
  if (stopping) return;
128
137
  stopping = true;
@@ -140,11 +149,20 @@ async function serve(pi, args) {
140
149
  const stopTimer = setInterval(async () => {
141
150
  try { if ((await readFile(stopPath, "utf8")).trim() === nonce) shutdown(); }
142
151
  catch { /* No stop request. */ }
152
+ if (stopping) return;
153
+ try {
154
+ const request = JSON.parse(await readFile(restartPath, "utf8"));
155
+ const prepared = JSON.parse(await readFile(updatePath, "utf8"));
156
+ if (request.nonce === nonce && request.launcher === prepared.launcher && existsSync(request.launcher)) {
157
+ restartRequest = request;
158
+ shutdown();
159
+ }
160
+ } catch { /* No valid restart request. */ }
143
161
  }, 400);
144
162
  process.on("SIGTERM", shutdown);
145
163
  process.on("SIGINT", shutdown);
146
164
  child.stdout?.on("data", (chunk) => {
147
- if (ready || opening || failed) return;
165
+ if (failed) return;
148
166
  buffer += chunk.toString("utf8");
149
167
  if (buffer.length > 8 * 1024 * 1024) { fail("Pi RPC 输出超出预期大小"); return; }
150
168
  for (let index; (index = buffer.indexOf("\n")) !== -1;) {
@@ -152,6 +170,11 @@ async function serve(pi, args) {
152
170
  buffer = buffer.slice(index + 1);
153
171
  let record;
154
172
  try { record = JSON.parse(line); } catch { continue; }
173
+ if (record?.type === "response" && /^dev-reload-\d+$/.test(record.id ?? "")) {
174
+ console.error(`pi-webapp: ${record.id} /${devReloadCommand ?? "web-dev-reload"}: ${record.success === true ? "reloaded" : `failed: ${record.error ?? "Unknown RPC error"}`}`);
175
+ continue;
176
+ }
177
+ if (ready || opening) continue;
155
178
  if (record?.type === "response" && record.id === "list-commands") {
156
179
  if (record.success !== true || !Array.isArray(record.data?.commands)) {
157
180
  fail("无法读取 Pi 扩展命令列表");
@@ -163,6 +186,8 @@ async function serve(pi, args) {
163
186
  fail("Pi 没有加载当前项目的 Web 扩展");
164
187
  return;
165
188
  }
189
+ devReloadCommand = record.data.commands.find((item) => item?.source === "extension"
190
+ && item.sourceInfo?.path === extension && /^web-dev-reload(?::\d+)?$/.test(item.name))?.name;
166
191
  child.stdin?.write(JSON.stringify({ id: "open-web", type: "prompt", message: `/${command.name}` }) + "\n");
167
192
  continue;
168
193
  }
@@ -175,6 +200,7 @@ async function serve(pi, args) {
175
200
  try { url = new URL(record.message.slice("pi-webapp: ".length)); }
176
201
  catch { continue; }
177
202
  if (url.hostname !== "127.0.0.1" || !url.hash) continue;
203
+ runningUrl = url.href;
178
204
  opening = true;
179
205
  stateWrite = saveState({ pid: process.pid, piPid: child.pid, nonce, cwd: process.cwd(), url: url.href })
180
206
  .then(() => {
@@ -182,6 +208,19 @@ async function serve(pi, args) {
182
208
  ready = true;
183
209
  clearTimeout(startupTimer);
184
210
  notifyParent({ type: "ready", pid: process.pid, url: url.href });
211
+ for (const root of pluginRoots) {
212
+ try {
213
+ devWatchers.push(watch(join(root, "dist"), (_event, file) => {
214
+ if (String(file ?? "") !== "extension.js") return;
215
+ clearTimeout(devReloadTimer);
216
+ devReloadTimer = setTimeout(() => {
217
+ if (closed || stopping) return;
218
+ if (!devReloadCommand) { console.error("pi-webapp: development reload unavailable: local extension command not found"); return; }
219
+ child.stdin?.write(JSON.stringify({ id: `dev-reload-${++devReloadCount}`, type: "prompt", message: `/${devReloadCommand}` }) + "\n");
220
+ }, 250);
221
+ }));
222
+ } catch (error) { console.error(`pi-webapp: cannot watch plugin ${root}: ${error}`); }
223
+ }
185
224
  })
186
225
  .catch((error) => fail(`无法保存启动状态:${error.message}`));
187
226
  return;
@@ -199,6 +238,8 @@ async function serve(pi, args) {
199
238
  });
200
239
  await stateWrite;
201
240
  clearTimeout(startupTimer);
241
+ clearTimeout(devReloadTimer);
242
+ for (const watcher of devWatchers) watcher.close();
202
243
  clearInterval(stopTimer);
203
244
  clearTimeout(killTimer);
204
245
  try { if ((await readFile(stopPath, "utf8")).trim() === nonce) await unlink(stopPath); } catch { /* Already removed. */ }
@@ -206,6 +247,42 @@ async function serve(pi, args) {
206
247
  try { if (JSON.parse(await readFile(statePath, "utf8")).nonce === nonce) await unlink(statePath); }
207
248
  catch { /* Already removed. */ }
208
249
  }
250
+ if (restartRequest && runningUrl) {
251
+ await unlink(restartPath).catch(() => {});
252
+ const oldUrl = new URL(runningUrl);
253
+ const fd = openSync(logPath, "a", 0o600);
254
+ const restartEnv = { ...process.env, PI_WEBAPP_AUTO_OPEN: "0", PI_WEBAPP_RESTART_PORT: oldUrl.port, PI_WEBAPP_RESTART_TOKEN: oldUrl.hash.slice(1) };
255
+ try {
256
+ const runLauncher = (path) => new Promise((resolveExit) => {
257
+ const next = spawnNode(process.execPath, [path, "start", ...args], { cwd: process.cwd(), env: restartEnv, stdio: ["ignore", fd, fd] });
258
+ next.once("error", () => resolveExit(1));
259
+ next.once("exit", (code) => resolveExit(code ?? 1));
260
+ });
261
+ if (await runLauncher(restartRequest.launcher) !== 0) {
262
+ await clearManagedLauncher(restartRequest.launcher);
263
+ console.error("pi-webapp: 新版启动失败,正在恢复旧版服务");
264
+ if (await runLauncher(launcher) !== 0) console.error(`pi-webapp: 旧版恢复失败。日志:${logPath}`);
265
+ }
266
+ } finally { closeSync(fd); }
267
+ }
268
+ }
269
+
270
+ async function clearManagedLauncher(path) {
271
+ try {
272
+ if (JSON.parse(await readFile(updatePath, "utf8")).launcher === path) await unlink(updatePath);
273
+ } catch { /* No matching managed release. */ }
274
+ }
275
+
276
+ async function managedLauncher() {
277
+ try {
278
+ if (existsSync(fileURLToPath(new URL("../.git", import.meta.url)))) return null;
279
+ const update = JSON.parse(await readFile(updatePath, "utf8"));
280
+ const current = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
281
+ if (!/^\d+\.\d+\.\d+$/.test(update.version) || !/^\d+\.\d+\.\d+$/.test(current.version)) return null;
282
+ if (!compatible(versionParts(update.version), versionParts(current.version))) return null;
283
+ if (update.version === current.version || !existsSync(update.launcher)) return null;
284
+ return update.launcher;
285
+ } catch { return null; }
209
286
  }
210
287
 
211
288
  async function start(args) {
@@ -215,6 +292,17 @@ async function start(args) {
215
292
  if (existing.url) await openPageIfLocal(existing.url);
216
293
  return;
217
294
  }
295
+ const managed = await managedLauncher();
296
+ if (managed) {
297
+ const next = spawnNode(process.execPath, [managed, "start", ...args], { cwd: process.cwd(), env: process.env, stdio: "inherit" });
298
+ const code = await new Promise((resolveExit, rejectExit) => { next.once("error", rejectExit); next.once("exit", (status) => resolveExit(status)); });
299
+ if (code !== 0) {
300
+ await clearManagedLauncher(managed);
301
+ console.error(`pi-webapp: 新版启动失败(代码 ${code ?? "未知"}),正在恢复当前安装`);
302
+ await start(args);
303
+ }
304
+ return;
305
+ }
218
306
  const nodeVersion = versionParts(process.versions.node);
219
307
  if (!nodeVersion || !compatible(nodeVersion, [22, 19, 0])) throw new Error("需要 Node.js 22.19 或更新版本。");
220
308
  if (!existsSync(extension)) throw new Error("找不到构建后的扩展。请先运行 npm run build。");