pi-webapp 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,6 +46,8 @@ In Pi, enter `/web` to open the interface in your default browser. Pi also print
46
46
 
47
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.
48
48
 
49
+ Chats run independently: switching workspaces or chats, or creating a new chat, leaves existing tasks running in the background. Returning to a chat restores its running output, history, and pending plugin interactions. Stop affects only the selected chat. Sessions remain in memory until the Pi service stops; stopping or restarting the service ends background tasks.
50
+
49
51
  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.
50
52
 
51
53
  ## Develop locally
package/README.zh-CN.md CHANGED
@@ -46,6 +46,8 @@ pi
46
46
 
47
47
  如果通过 Pi 终端手动输入 `/web`,该终端会话会与浏览器保持同步。使用启动器时,Pi 在后台运行。在其他工作区打开的会话使用 Pi SDK,并加载该工作区的技能和项目上下文;已启用的 Web 插件 Pi 入口会显式加载,其他普通 Pi 扩展仍与主会话隔离。
48
48
 
49
+ 每个聊天独立运行:切换工作区、切换聊天或新建聊天时,原任务会继续在后台执行。切回后会恢复正在输出的内容、历史记录和待处理的插件交互。“停止”只影响当前选中的聊天。会话实例会保留到 Pi 服务停止;停止或重启服务会结束后台任务。
50
+
49
51
  启动时会从 Pi 已保存的会话中发现工作区,因此先前在其他目录使用 Pi 的会话也会出现在侧边栏。工作区记录和附件继续存放在 Pi 现有的 `pi-web/` 数据目录下。从侧边栏移除工作区不会删除其文件或会话;移除后如需再次显示,可以手动添加。
50
52
 
51
53
  ## 本地开发
package/bin/pi-webapp.mjs CHANGED
@@ -131,26 +131,43 @@ async function serve(pi, args) {
131
131
  let runningUrl = null;
132
132
  let buffer = "";
133
133
  let killTimer;
134
+ let stopReason;
135
+ let forceStopRequested = false;
136
+ const instanceId = randomUUID();
137
+ const startedAt = performance.now();
138
+ const lifecycle = (event, fields = {}) => {
139
+ try {
140
+ console.error(JSON.stringify({ timestamp: new Date().toISOString(), event,
141
+ level: event === "pi_process_exit" && (!stopping || failed) ? "error" : "info",
142
+ instanceId, pid: process.pid, piPid: child.pid, uptimeMs: Math.round(performance.now() - startedAt), ...fields }));
143
+ } catch { /* Diagnostics must not interrupt process supervision. */ }
144
+ };
134
145
  const devWatchers = [];
135
146
  let devReloadTimer;
136
147
  let devReloadCount = 0;
137
148
  let devReloadCommand;
138
- const shutdown = () => {
149
+ const shutdown = (reason) => {
139
150
  if (stopping) return;
140
151
  stopping = true;
152
+ stopReason = reason;
153
+ lifecycle("launcher_stop_requested", { reason });
141
154
  child.stdin?.end();
142
- killTimer = setTimeout(() => child.kill("SIGTERM"), 5000);
155
+ killTimer = setTimeout(() => {
156
+ forceStopRequested = true;
157
+ lifecycle("pi_force_stop", { reason, signal: "SIGTERM" });
158
+ child.kill("SIGTERM");
159
+ }, 5000);
143
160
  killTimer.unref();
144
161
  };
145
162
  const fail = (message) => {
146
163
  if (failed || ready) return;
147
164
  failed = true;
148
165
  notifyParent({ type: "error", message });
149
- shutdown();
166
+ shutdown("startup_failure");
150
167
  };
151
168
  const startupTimer = setTimeout(() => fail(`Pi 启动超时。日志:${logPath}`), 60000);
152
169
  const stopTimer = setInterval(async () => {
153
- try { if ((await readFile(stopPath, "utf8")).trim() === nonce) shutdown(); }
170
+ try { if ((await readFile(stopPath, "utf8")).trim() === nonce) shutdown("stop_request"); }
154
171
  catch { /* No stop request. */ }
155
172
  if (stopping) return;
156
173
  try {
@@ -158,12 +175,12 @@ async function serve(pi, args) {
158
175
  const prepared = JSON.parse(await readFile(updatePath, "utf8"));
159
176
  if (request.nonce === nonce && request.launcher === prepared.launcher && existsSync(request.launcher)) {
160
177
  restartRequest = request;
161
- shutdown();
178
+ shutdown("restart_request");
162
179
  }
163
180
  } catch { /* No valid restart request. */ }
164
181
  }, 400);
165
- process.on("SIGTERM", shutdown);
166
- process.on("SIGINT", shutdown);
182
+ process.on("SIGTERM", () => shutdown("signal_sigterm"));
183
+ process.on("SIGINT", () => shutdown("signal_sigint"));
167
184
  child.stdout?.on("data", (chunk) => {
168
185
  if (failed) return;
169
186
  buffer += chunk.toString("utf8");
@@ -210,6 +227,7 @@ async function serve(pi, args) {
210
227
  if (closed || failed) return;
211
228
  ready = true;
212
229
  clearTimeout(startupTimer);
230
+ lifecycle("launcher_ready", { port: Number(url.port) });
213
231
  notifyParent({ type: "ready", pid: process.pid, url: url.href });
214
232
  for (const root of pluginRoots) {
215
233
  try {
@@ -233,8 +251,11 @@ async function serve(pi, args) {
233
251
  child.stdin?.write(JSON.stringify({ id: "list-commands", type: "get_commands" }) + "\n");
234
252
  await new Promise((resolveExit) => {
235
253
  child.once("error", (error) => { fail(`无法启动 Pi:${error.message}`); resolveExit(); });
236
- child.once("close", (code) => {
254
+ child.once("close", (code, signal) => {
237
255
  closed = true;
256
+ lifecycle("pi_process_exit", { ready, exitCode: code, signal,
257
+ reason: stopReason ?? (ready ? "unexpected_exit" : "startup_failure"), forceStopRequested,
258
+ port: runningUrl ? Number(new URL(runningUrl).port) : null });
238
259
  if (!ready) fail(`Pi 提前退出(代码 ${code ?? "未知"})。日志:${logPath}`);
239
260
  resolveExit();
240
261
  });